Otvorila monádu a ... našla v nej celý výpočet! | robonovotny
source link: https://novotnyr.github.io/scrolls/otvorila-monadu-a-nasla-v-nej-cely-vypocet/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Otvorila monádu a … našla v nej celý výpočet!
2021/09/26
Otvorila som monádu a ostala som v šoku! Bola v nej celá história!
Výpočty robíme bežne, napríklad:
var message = "twinkle twinkle little star";
message = message.toUpperCase();
message = message.replace(" ", "");
message = message.substring(0, 14)
Čo keby sme však chceli vedieť, aký je výsledok po každom kroku? To by sme museli všade napchať logovanie:
var message = "twinkle twinkle little star";
message = message.toUpperCase();
log("Uppercase: " + message);
message = message.replace(" ", "");
log("Bez medzier: " + message);
message = message.substring(0, 14)
log("Prvých 14 znakov: " + message);
S použitím monády to môžeme spraviť oveľa lepšie: nemusíme rozsievať logovacie hlášky a už vôbec nemusíme upravovať každú z funkcií.
Trieda Writer
Keďže máme funkcie, ktoré vykonávame po sebe, a činnosti medzi nimi chceme obohatiť o dodatočný kód, môžeme použiť návrhový vzor monáda.
Refrén po minulých dieloch:
- potrebujeme triedu, ktorá obalí nejaký generický dátový typ
R
. - potrebujeme metódu, ktorá bežný objekt zabalí do triedy z predošlého bodu
- potrebujeme metódu, ktorá prijme funkciu, čo vybalí vnútro, čosi spočíta a vráti nový zabalený objekt do triedy z prvého bodu
Naša trieda – nazvime ju Writer
– bude iným prípadom oproti Maybe
alebo zoznamu – pretože si bude pamätať dve veci:
- nejakú hodnotu, čo bude výsledok posledného „výpočtového kroku“ – napr. REŤAZEC s veľkými písmenami alebo
reťazecbezmedzier
. - log, teda zoznam logovacích hlášok, ktoré sa udiali počas predošlých výpočtových krokov.
Prvý nástrel!
package com.github.novotnyr.monad.writer;
import java.util.ArrayList;
import java.util.List;
public class Writer<T> {
private T value;
private List<String> log = new ArrayList<>();
public static <T> Writer<T> log(T value, String message) {
Writer<T> writer = new Writer<>();
writer.value = value;
writer.log.add(message);
return writer;
}
}
Trieda je len glorifikovaná usporiadaná dvojica (hodnota a log).
Pomocná metóda log
je zase glorifikovaný konštruktor, ale takto to bude lepšie vyzerať v testoch.
Metóda pre zreťazenie
A teraz to dôležité: metóda pre zreťazenie!
public <Result> Writer<Result> then(Function<Value, Writer<Result>> transformer)
Metóda zoberie hodnotu typu Value
a funkciu z Value
do výsledkov typu Result
– ale v obale – a celý nový obalený výpočet vráti.
Aby sme dodržali konvencie v Jave, generické typy skrátime: Result
na R
a hodnoty Value
na T
.
public <R> Writer<R> then(Function<T, Writer<R>> transformer)
Idea v kóde je nasledovná:
- Použijeme funkciu na vnútro, získame obalený nový
Writer
. - Vytiahneme z neho hodnotu a zapamätáme si ju.
- Vytiahneme z neho log (je to zoznam), a nalepíme ho na koniec aktuálneho logu.
- Aj hodnotu, aj celý nový log zabalíme do nového
Writer
-a, ktorý pošleme von ako výsledok.
Naprogramujme to!
public <R> Writer<R> then(Function<T, Writer<R>> transformer) {
Writer<R> transformedWriter = transformer.apply(this.value);
var newWriter = new Writer<R>();
newWriter.value = transformedWriter.value;
// zlepíme oba logy, metódu dorobíme o chvíľu!
newWriter.log = concatenate(this.log, transformedWriter.log);
return newWriter;
}
Celý tanec robíme hlavne preto, aby sme garantovali nemennosť (immutability) každého z objektov, čo predíde mnohým (mnohým!) problémom.
Ešte musíme dopracovať metódu concatenate
:
private static <T> List<T> concatenate(List<T> list1, List<T> list2) {
List<T> result = new ArrayList<>(list1.size() + list2.size());
result.addAll(list1);
result.addAll(list2);
return result;
}
A ako bonus, nezabudnime na getter, ktorým vrátime celý log:
public List<String> getLog() {
return log;
}
Otestujme si monádu
Teraz si to všetko otestujme!
package com.github.novotnyr.monad.writer;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static com.github.novotnyr.monad.writer.Writer.log;
import static org.junit.jupiter.api.Assertions.assertEquals;
class WriterTest {
@Test
public void test() {
AtomicReference<String> result = new AtomicReference<>();
Writer<String> writer = log("twinkle twinkle little star", "START")
.then(s -> log(s.toUpperCase(), "To upper case: [" + s + "]"))
.then(s -> log(s.replace(" ", ""), "Remove spaces: [" + s + "]"))
.then(s -> log(s.substring(0, 14), "First fourteen: [" + s + "]"))
.then(s -> {
result.set(s);
return log(s, "EOF");
});
List<String> log = writer.getLog();
assertEquals("TWINKLETWINKLE", result.get());
assertEquals(5, log.size());
System.out.println(log);
}
}
Logovanie začneme pevným reťazcom, ktorý postupne transformujeme vrátane logovacích hlášok.
Ak sa pozrieme na výsledný log, bude vyzerať nasledovne:
START
To upper case: [twinkle twinkle little star]
Remove spaces: [TWINKLE TWINKLE LITTLE STAR]
First fourteen: [TWINKLETWINKLELITTLESTAR]
EOF
Sprehľadnenie zápisov
Zápis môžeme skrátiť ďalšou užitočnou metódou vo triede Writer
:
public static <T> Writer<T> logResult(T result, String description) {
return log(result, description + ": [" + result + "]");
}
Test sa potom skráti:
@Test
public void testWithHelperMethod() {
AtomicReference<String> result = new AtomicReference<>();
Writer<String> writer = log("twinkle twinkle little star", "START")
.then(s -> logResult(s.toUpperCase(), "To upper case"))
.then(s -> logResult(s.replace(" ", ""), "Remove spaces"))
.then(s -> logResult(s.substring(0, 14), "First fourteen"))
.then(s -> {
result.set(s);
return log(s, "EOF");
});
List<String> log = writer.getLog();
assertEquals("TWINKLETWINKLE", result.get());
assertEquals(5, log.size());
}
Ešte viac sprehľadnenia
Tento zápis nie je úplne ideálny. Je viac spôsobov, ako ho skrátiť, a jeden z nich je vytiahnuť funkcie do premenných:
@Test
public void testWithFunctions() {
AtomicReference<String> result = new AtomicReference<>();
Function<String, Writer<String>> toUpperCase = s -> logResult(s.toUpperCase(), "To upper case");
Function<String, Writer<String>> removeSpaces = s -> logResult(s.replace(" ", ""), "Remove spaces");
Function<String, Writer<String>> firstFourteen = s -> logResult(s.substring(0, 14), "First fourteen");
Writer<String> writer = log("twinkle twinkle little star", "START")
.then(toUpperCase)
.then(removeSpaces)
.then(firstFourteen)
.then(s -> {
result.set(s);
return log(s, "EOF");
});
List<String> log = writer.getLog();
assertEquals("TWINKLETWINKLE", result.get());
assertEquals(5, log.size());
System.out.println(log);
}
Aj toto by sa ešte dalo skrátiť, ale to by sme sa dostali do krajiny kompozícií funkcií, na čo teraz nemáme čas.
Funkcie sú teraz elegantne zreťazené a všetko sa loguje správne!
Rúry logovaných funkcií
Pre odvážlivcov môžeme pripraviť dvojicu užitočných metód: start()
a pipe()
:
Metóda start
v triede Writer
len obalí výsledok s prázdnou hláškou.
public static <T> Writer<T> start(T value) {
return log(value, "");
}
Metóda pipe
zavolá reťazec funkcií a začne logovať:
@SafeVarargs
public final Writer<T> pipe(Function<T, Writer<T>>... transformers) {
Writer<T> intermediateWriter = this;
for (Function<T, Writer<T>> transformer : transformers) {
intermediateWriter = intermediateWriter.then(transformer);
}
return intermediateWriter;
}
A kód potom vyzerá už celkom milo:
Function<String, Writer<String>> toUpperCase = s -> logResult(s.toUpperCase(), "To upper case");
Function<String, Writer<String>> removeSpaces = s -> logResult(s.replace(" ", ""), "Remove spaces");
Function<String, Writer<String>> firstFourteen = s -> logResult(s.substring(0, 14), "First fourteen");
var writer = start("twinkle twinkle little star")
.pipe(toUpperCase, removeSpaces, firstFourteen)
Tu si utešene vytvoríme rúru (pipe) a dáta prepasírujeme cez viaceré funkcie, pričom po ceste vyrábame log!
Záver
Vidíme, že monáda môže fungovať aj nad viacerými zložkami naraz – monáda Writer
ukazuje príklad „programovateľnej bodkočiarky“, kde sa medzi jednotlivými krokmi programu automaticky dejú ľubovoľné veci – napríklad zápis medzivýsledkov do logu.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK