Sofronije vs. monade

5 min

Ništa ti, sinak, ono sa monadama ne razumem. Oš li mi, ovaj, ti to malo pojasniti? Pratim kurs “Postani Java Pevac Ekspert za 21. dan kukurikanja”; još koje jutro i gotov sam! Hajde sinak, neću ni reč reći, sve ću te slušati, ja kada ćutim - onda ćutim, ništa ne govorim, ja sam lepo vaspitan, jednom umalo nisam umro od gladi jer nisam hteo da kažem da sam gladan…

- (uzdah) Hajde, Sofronije, da probamo primer koji sam jednom video u onom YT kokošinjcu. Ovako nekako ide.

Vidi ove dve funkcije:

public Integer square(Integer i) {
  return i * i;
}
public Integer increment(Integer i) {
  return i + 1;
}

Funkcije možeš da kombinuješ, na primer:

increment(square(3)); // 3*3+1=10

Sada zamisli da je kvočka Mileva zatražila da prati šta se dešava tokom računanja, kako bi proverila da je sve kako treba.

- Vala baš, sinak, ta Mileva samo zrna broji vasceli dan, umesto muža da traži. Dobro, sinak, kako ćemo da to rešimo?

- Menjamo funkcije, Sofronije; treba da nam vrate više toga nazad. Uvodimo nam novi tip kojim se vraćaju podaci nazad iz funkcija. Na primer ovako:

public class IntegerWithHistory {
  public final Integer value;
  public final String[] history;
  // ctor...
}

public IntegerWithHistory square(Integer i) {
  return new IntegerWithHistory(
    i * i,
    new String[] {"square: " + i}
  );
}
public IntegerWithHistory increment(Integer i) {
  return new IntegerWithHistory(
    i + 1,
    new String[] {"increment: " + i}
  );
}

- Ček, ovaj, ček malo, sinak… pa kako ćemo sada da kombinujemo funkcije, sto mu petlova?

- Bravo Sofronije, dobro si primetio! Moramo da promenimo i ulazni tip za funkcije:

public IntegerWithHistory square(IntegerWithHistory i) {
  return new IntegerWithHistory(
    i.value * i.value,
    join(i.history, "square: " + i.value)
  );
}
public IntegerWithHistory increment(IntegerWithHistory i) {
  return new IntegerWithHistory(
    i.value + 1,
    join(i.history, "increment: " + i.value)
  );
}

Sada je kombinovanje moguće:

IntegerWithHistory i =
  increment(square(
    new IntegerWithHistory(3, new String[] {}))
);
System.out.println(i.value);
System.out.println(Arrays.toString(i.history));

- Stani, ovaj, stani malo sinak: pa zar pravimo objekat IntegerWithHistory za svaki broj sa kojim radimo?

- Pravimo, Sofronije, nego šta. Ali nećemo stalno ovako pisati, svakako, nego pravimo funkciju za to. Neka se zove wrap(). To je nekakav statički konstruktor našeg tipa. Najviše ima smisla dodati je u sam IntegerWithHistory:

public class IntegerWithHistory {
  public final Integer value;
  public final String[] history;
  // ctor
  public static IntegerWithHistory wrap(Integer i) {
    return new IntegerWithHistory(i, new String[] {});
  }
}
//...
IntegerWithHistory i =
  increment(square(IntegerWithHistory.wrap(3)));

Okej, to je bilo lako. Sofronije, pazi sada: zagledaj se u ove dve funkcije square i increment, da li nešto primećuješ?

- Kao da su, možda, slične ko jaje jajetu?

- Tako je. Hajde da označimo šta je različito:

Ako bi ove različite blokove nekako mogli da prosledimo kao argumente, ostala bi samo jedna funkcija!

- Znam, ovaj, znam! Daj meni da to uradim, dečače; duda-duda-de, otvori oči i gledaj:

public IntegerWithHistory calculate(
    IntegerWithHistory i,
    Function<Integer, Integer> operation,
    String operationName) {
  return new IntegerWithHistory(
    operation.apply(i.value),
    join(i.history, operationName + ": " + i.value)
  );
}

- Vrlo blizu, Sofronije; ali ne. Sad ti treba puna pažnja; batali zafrkavanje tog kera i gledaj ovamo.

Već imamo strukturu IntegerWithHistory koja nam čuva vrednost i nekakav tekst - hajde da to zapišemo nekako ovako: {Integer, String}. S druge strane, treba nam funkcija za vrednost i taj isti tekst: {(Integer→Integer), String}. Kako da iz prvog dobijemo ovo drugo? Kako da iskoristimo istu strukturu (tip) IntegerWithHistory, a da imamo i funkciju kojom menjamo vrednost?

{Integer, String} => {(Integer→Integer), String}

Tako što ćemo (Integer→_) da izvučemo iz drugog dela izraza i primenimo ga na prvi :))) Šalim se malo, ali i ne. Drugim rečima, treba nam funkcija: (Integer→{Integer,String}):

Function<Integer, IntegerWithHistory> increment =
  i -> new IntegerWithHistory(i + 1, "increment: " + i);
Function<Integer, IntegerWithHistory> square =
  i -> new IntegerWithHistory(i * i, "square: " + i);

Videli smo da nije dovoljno da imamo funkciju koja samo menja vrednost, jer ne zna ništa o istoriji. Na ovaj način koristimo postojeći tip IntegerWithHistory (vrednost i istorija) i dodajemo transformaciju vrednosti! Time dva potrebna argumenta za funkciju calculate postaju jedan:

public IntegerWithHistory calculate(
    IntegerWithHistory i,
    Function<Integer, IntegerWithHistory> operation) {
  final IntegerWithHistory a = operation.apply(i.value);
  return new IntegerWithHistory(
    a.value,
    join(i.history, a.history)
  );
}

Koristi se ovako:

IntegerWithHistory i = calculate(
  calculate(IntegerWithHistory.wrap(3), square),
  increment);

- Auuu, sinak, moraću malo da, ovaj, da razmislim o svemu. Vidim šta si uradio, jasno mi je; mada i nije nešto lepo. Nego, jel’ smo gotovi? Mileva me zove u kokošinjac na čaj, ostala je sama.

- Hajde još samo da prebacimo calculate() u IntegerWithHistory. To je bar lako:

public static class IntegerWithHistory {
  public final Integer value;
  public final String[] history;
  // ctor

  public static IntegerWithHistory wrap(Integer i) {
    return new IntegerWithHistory(i);
  }

  public IntegerWithHistory calculate(Function<Integer, IntegerWithHistory> operation) {
    final IntegerWithHistory a = operation.apply(value);
    return new IntegerWithHistory(
      a.value,
      join(history, join(history, a.history))
    );
  }
}

Gledaj kako sada izgleda program:

val increment =
  i -> new IntegerWithHistory(i + 1, "increment: " + i);
val square =
  i -> new IntegerWithHistory(i * i, "square: " + i);

val i = wrap(3).calculate(square).calculate(increment);

Nije li sjajno!?

- Kukurikuuuu jeste, dečače, sto mu gromova, baš u stilu Dejvida Kroketa! Nego, sinak, gde su ti tu te monade druškane, šta me sada zamajavaš sa ovim računanjima?

- Sofronije, upravo smo napravili monadu: IntegerWithHistory. Funkcija wrap je unit(). Funkcija calculate je flatMap(). Napravili smo kontejner za broj koji nosi dodatni kontekst oko njega - istoriju.

- Auuu, prevca ti… Ovaj, dečko, imaš li još onih mudrijaških knjiga?

🧧
Nisam definisan svojim stavovima. Stavove usvajamo, menjamo, nadograđujemo, ali oni ne čine nas same. Manje je važno da li se slažemo, koliko da se razumemo.