scalaz-stream Radek Miček
Obsah 1. Jednoduché zpracování proudů C# + standardní knihovna vs Scala + scalaz-stream 2. Použití knihovny scalaz-stream v naší aplikaci O naší aplikaci Architektura naší aplikace Příklad: Služba pro ukládání historie hovorů 3. Srovnání S aktory (známými z Erlangu) S líným vstupem (známým z Haskellu) 16. dubna 2014 2
1. Jednoduché zpracování proudů 16. dubna 2014 3
scalaz-stream Knihovna pro zpracování proudů dat Proud dat = posloupnost dat, kde data přicházejí postupně a může jich být i nekonečně mnoho. Společnost Spinoco knihovnu scalaz-stream používá a spoluvyvíjí https://github.com/scalaz/scalaz-stream 16. dubna 2014 4
Př.: Čtení souboru (C# TextReader) Zadání: Načíst prvních 100 řádek souboru. var result = new List<string>(); using (var r = File.OpenText("a.txt")) { int i = 0; var line = ""; while ((line = r.readline())!= null && i < 100) { result.add(line); i++; } } 16. dubna 2014 5
Př.: Čtení souboru (C# TextReader) Zadání: Načíst prvních 100 řádek souboru, jenž začínají slovem 'Spinoco'. var result = new List<string>(); using (var r = File.OpenText("a.txt")) { int i = 0; var line = ""; while ((line = r.readline())!= null && i < 100) { if (line.startswith("spinoco")) { result.add(line); i++; } } } 16. dubna 2014 6
C# TextReader Nepřehledný kód měli bychom oddělit filtrování a omezování počtu řádek Filtrování dle predikátu (např. řádka začíná slovem 'Spinoco') nebo omezování počtu vrácených řádek (např. 100 prvních řádek) jsou často požadované funkcionality. Mohly by tedy existovat funkce, jenž transformují daný TextReader např.: TextReader FilterLines(TextReader r, Predicate<string> p) nebo TextReader LimitLines(TextReader r, int n). Standardní knihovna však funkce pro transformaci TextReaderu neobsahuje (a počet metod ve třídě TextReader nijak neusnadňuje psaní vlastních). TextReader je pouze pro čtení textových proudů dat Proč se ale omezovat na znaky a řetězce? 16. dubna 2014 7
C# IEnumerable / Java Iterable Rozhraní pro procházení posloupností prvků. 16. dubna 2014 8
Př.: Čtení souboru (C# IEnumerable) Zadání: Načíst prvních 100 řádek souboru, jenž mají prefix 'Spinoco'. var result = File.ReadLines("a.txt").Where(line => line.startswith("spinoco")).take(100).tolist(); Obyčejné volání metod var result = (from line in File.ReadLines("a.txt") where line.startswith("spinoco") select line ).Take(100).ToList(); Speciální syntax připomínající SQL 16. dubna 2014 9
Rozbor: Čtení souboru (C# IEnumerable) Zadání: Načíst prvních 100 řádek souboru, jenž mají prefix 'Spinoco'. var result = File.ReadLines("a.txt").Where(line => line.startswith("spinoco")).take(100).tolist(); Kód je přehledný IEnumerable je obvykle líné Ale v tomto případě, se soubor otevře ihned Metoda ReadLines není líná (.NET Framework 4.5.1) viz http://referencesource.microsoft.com/#mscorlib/system/io/readlinesiterator.cs Metody Where a Take transformují IEnumerable 16. dubna 2014 10
Př.: Zápis souborů (C# IEnumerable) Zadání: Buď lines instance IEnumerable<string>. Do prvního souboru chceme zapsat všechny řádky z lines a do druhého souboru jen ty, které začínají slovem 'Spinoco'. File.WriteAllLines("a.txt", lines); File.WriteAllLines( "b.txt", lines.where(line => line.startswith("spinoco"))); lines čteme dvakrát 16. dubna 2014 11
Př.: Zápis souborů (C# IEnumerable) Zadání: Buď lines instance IEnumerable<string>. Do prvního souboru chceme zapsat všechny řádky z lines a do druhého souboru jen ty, které začínají slovem 'Spinoco'. using (var w1 = File.CreateText("a.txt")) using (var w2 = File.CreateText("b.txt")) { foreach (var line in lines) { w1.writeline(line); if (line.startswith("spinoco")) { w2.writeline(line); } } } lines čteme jen jednou, ale řešení už není přehledné 16. dubna 2014 12
Př.: Uvolňování zdrojů (C# IEnumerable) Zadání: Buď lines instance IEnumerable<string>. Chceme vypsat každý druhý prvek lines. using (var e = lines.getenumerator()) { for (e.movenext(); e.movenext(); e.movenext()) { Console.WriteLine(e.Current); } } Bez using by hrozilo, že zdroje alokované enumerátorem nebudou uvolněny. 16. dubna 2014 13
C# IEnumerable Přehledný kód pro čtení a filtrování Speciální podpora v kompilátoru (yield return a yield break) Nedostatečně řeší zápis Lze zlepšit Uvolňování zdrojů závisí na kázni programátora Lepší by bylo, kdyby knihovna byla navržena tak, aby nemohlo dojít k úniku zdrojů Typové systémy některých jazyků (ATS, Rust, ParaSail) umí pomoci Scala ani C# takový typový systém nemají 16. dubna 2014 14
Jak scalaz-stream řeší uvolňování zdrojů? 16. dubna 2014 15
Jak scalaz-stream řeší uvolňování zdrojů? Sama je alokuje i dealokuje 16. dubna 2014 16
Př.: Čtení souboru (scalaz-stream) Zadání: Načíst prvních 100 řádek souboru, jenž mají prefix 'Spinoco'. val result = io.linesr("a.txt").filter(_.startswith("spinoco")).take(100).runlog.run Výrazy io.linesr("a.txt") io.linesr("a.txt").filter(_.startswith("spinoco")) io.linesr("a.txt").filter(_.startswith("spinoco")).take(100) mají typ Process[Task, String], a proto jim budeme říkat procesy Proces je líný na rozdíl od IEnumerable to platí vždy Soubor se neotevře, dokud to není třeba Otevření souboru a načtení řádek je vynuceno voláním runlog.run Metody filter a take transformují proces 16. dubna 2014 17
Př.: Čtení souboru (scalaz-stream) Zadání: Načíst prvních 100 řádek souboru, jenž mají prefix 'Spinoco'. val result = io.linesr("a.txt").filter(_.startswith("spinoco")).take(100).runlog.run scalaz-stream var result = File.ReadLines("a.txt").Where(line => line.startswith("spinoco")).take(100).tolist(); C# IEnumerable 16. dubna 2014 18
Př.: Transformace řádků (scalaz-stream) Zadání: Načíst délky řádek v souboru. val result = io.linesr("a.txt").map(_.length).runlog.run map transformuje řádky pomocí dané funkce V našem příkladu je funkce: řetězec Typy výrazů délka řetězce io.linesr("a.txt") : Process[Task, String] io.linesr("a.txt").map(_.length): Process[Task, Int] 16. dubna 2014 19
Př.: Čtení dvou souborů Zadání: Soubor mesta.txt obsahuje hlavní města zemí ze souboru zeme.txt (ve stejném pořadí). Cílem je spárovat země s jejich hlavními městy a výsledek zapsat do jiného souboru. val result = io.linesr("zeme.txt").zip(io.linesr("mesta.txt")).runlog.run result je posloupnost dvojic země město zip kombinuje dva procesy dohromady Opakovaně: Přečte jeden prvek z prvního vstupu (např. zeme.txt), jeden prvek ze druhého vstupu (např. mesta.txt), vytvoří dvojici a pošle ji dál Oba soubory jsou automaticky otevřeny i uzavřeny 16. dubna 2014 20
Př.: Výpis souboru (scalaz-stream) Zadání: Vypsat řádky souboru. io.linesr("a.txt").to(io.stdoutlines).run.run io.stdoutlines je sink, který vypisuje příchozí řetězce jako řádky na standardní výstup Řádky do sinku posíláme metodou to Použitím metody to řádky zmizí v sinku, místo nich zůstane hodnota () typu Unit (typ Unit je něco jako typ void, ale na rozdíl od něj má jedinou hodnotu) Metoda run (první) ignoruje výstup procesu (na rozdíl od metody runlog, jenž výstup procesu ukládá a vrací) Proč používáme run místo runlog? 16. dubna 2014 21
Př.: Zápis souborů (scalaz-stream) Zadání: Buď lines instance Process[Task, String]. Do prvního souboru chceme zapsat všechny řádky z lines a do druhého souboru jen ty, které začínají slovem 'Spinoco'. def linesw(filename: String): Sink[Task, String] = io.filechunkw(filename).pipein(text.utf8encode) lines.observe(linesw("a.txt")).filter(_.startswith("spinoco")).to(linesw("b.txt")).run.run Metoda observe pošle řádky do sinku Na rozdíl od metody to řádky nezmizí (dále s nimi můžeme pracovat) linesw je sink, který zapisuje řádky do souboru (bohužel není součástí knihovny) 16. dubna 2014 22
Co knihovna scalaz-stream umí? Transfromace prvků map, collect, Filtrování filter, drop, take, dropwhile, takewhile, droplast Agregování reduce, fold, exists, forall, Kombinování procesů append, repeat deterministicky: pipe, zip, zipwith, tee, nedeterministicky: merge, wye, mergen, 16. dubna 2014 23
Co knihovna scalaz-stream umí? Bezpečná práce se zdroji linesr, filechunkr, filechunkw, resource, Odlišení procesů s vedlejšími efekty (pomocí Task) Process[Task, A], Sink[Task, A], Channel[Task, A, B], A další flatmap, intersperse, awakeevery, 16. dubna 2014 24
scalaz-stream vs C# IEnumerable scalaz-stream automatická alokace i dealokace zdrojů nedeterministické čtení z více vstupů lepší podpora zápisu odlišení procesů s vedlejšími efekty pomocí typových signatur větší množství funkcí pro skládání a transformace procesů C# IEnumerable speciální podpora v kompilátoru 16. dubna 2014 25
2. Použití knihovny scalaz-stream v naší aplikaci 16. dubna 2014 26
Naše vize Jedna aplikace pro různé komunikační kanály Telefonní hovory Chat E-mail SMS Sociální sítě Funkce Historie komunikace On-line monitoring Statistiky Rozdělování úkolů a plánování času 16. dubna 2014 27
Architektura naší aplikace Front end JavaScript, AngularJS, HTML5 Back end Scala, scalaz-stream, Play Apache Kafka, Apache Cassandra 16. dubna 2014 29
Hledáme nové kolegy 1. Vývojáře ve Scale 2. Vývojáře v JavaScriptu U nás se skutečně programuje funkcionálně Je možné pracovat na zkrácený úvazek Pro více informací pište na create@spinoco.com 16. dubna 2014 30
Back end Webový server + supervizor + služby Propojeno pomocí procesů z knihovny scalaz-stream Webový server autentizuje uživatele a jejich požadavky předává konkrétním službám Supervizor startuje služby, dohlíží na ně a v případě potřeby je restartuje Služba Umí obnovit stav po restartu Vstupem může být výstup jiných služeb 16. dubna 2014 31
Př.: Služba pro ukládání historie hovorů K událostem o telefonních hovorech doplňuje uživatele, ukládá je do databáze a poskytuje je dalším službám (např. pro výpočet statistik) Vstupy Události o telefonních hovorech Stav telefonů kdo je právě přihlášen na každém telefonu a od kdy (spojitý proces prvek načtený z procesu obsahuje aktuální stav telefonů, vždy je možné načíst prvek) 16. dubna 2014 32
Př.: Služba pro ukládání historie hovorů Párování událostí o hovorech s uživateli: val calleventswithuser = callevents.zip(phoneswithuser.continuous).map(finduserfromphonemap).through(pairfromhistory(finduser).onlyif(_.user.isempty)) callevents je proces událostí o hovorech phoneswithuser.continuous je spojitý proces s aktuálním stavem telefonů zip vytvoří proces dvojic (událost o hovoru, aktuální stav telefonů) finduserfromphonemap bere dvojici (událost o hovoru, aktuální stav telefonů) a pokud může, přiřadí události o hovoru uživatele through je jako map, ale navíc může provést vedlejší efekt (např. hledání v DB) finduser najde v databázi uživatele, který byl přihlášen na telefonu v daný čas pairfromhistory přiřadí uživatele z DB události o hovoru 16. dubna 2014 33
Př.: Služba pro ukládání historie hovorů Ukládání událostí o hovorech do databáze: calleventswithuser.observe(storeevent) storeevent je sink, jenž uloží událost o hovoru do databáze Pokud událost projde za observe, je garantováno, že je uložena v databázi 16. dubna 2014 34
Obnovení stavu služby po restartu Komunikace mezi službami je posílána přes Apache Kafku Apache Kafka je systém pro posílání zpráv Podobá se např. RabbitMQ, na rozdíl od něj však garantuje trvanlivost zpráv (při vhodném nastavení) tj. zprávy se neztratí, když zápis do Kafky uspěje Zprávy se publikují (zapisují) do tzv. topiců Publikováním je zprávě automaticky přiřazeno pořadové číslo (offset) Více služeb může publikovat do jednoho topicu Více služeb může číst z jednoho topicu (i zprávy s různými offsety) Stav služby se jednou za čas ukládá do databáze společně s offsety naposledy zpracovaných zpráv z Kafky 16. dubna 2014 35
Obnovení stavu služby po restartu Po restartu služby se načte poslední uložený stav služby z databáze společně s offsetem naposledy zpracované zprávy z Kafky Služba se připojí ke Kafce a začne číst z následujícího offsetu 16. dubna 2014 36
Výhody knihovny scalaz-stream Požadovaná funkcionalita se poskládá použitím jednoduchých a znovupoužitelných funkcí Kód je lépe čitelný Služby nezávisí na implementaci databáze nebo Kafky Služby ani neví, že používají Apache Cassandru nebo Apache Kafku při konstrukci dostávají obyčejné procesy Výrazně usnadňuje testování (např. lze snadno nasimulovat selhání procesu, ale je těžké nasimulovat selhání Kafky nebo Cassandry) Typy popisují kód 16. dubna 2014 37
3. Srovnání 16. dubna 2014 38
Aktory Aktor je objekt, jenž zprávy zpracovává sekvenčně Aktor přijímá zprávy, na základě přijatých zpráv mění svůj vnitřní stav, provádí vedlejší efekty a posílá zprávy jiným aktorům Služba by byla aktor, kdybychom aktory používali 16. dubna 2014 39
scalaz-stream vs aktory scalaz-stream Pull z procesu se čte, jen když je to třeba Procesy se lehce kombinují pomocí jednoduchých funkcí existující proces můžeme využít jako součást procesů, jenž vytváříme Procesy se automaticky starají o alokaci a dealokaci zdrojů Z typu procesu jde poznat jeho vstup, výstup a i to, zda provádí vedlejší efekty Aktory Push aktor běží a posílá zprávy dalším aktorům, i když třeba na žádné zprávy nečekají Některé implementace aktorů (Erlang, Akka, Orleans) mají zabudovanou podporu pro dohled nad aktory (supervizor) Většina implementací aktorů nevyužívá typový systém z typu aktoru typicky nejde poznat, zda aktor dělá vedlejší efekty nebo jaké zprávy posílá jiným aktorům 16. dubna 2014 40
Líný vstup v Haskellu Funkce readfile načte obsah souboru jako řetězec (tj. seznam znaků) Ve skutečnosti čtení souboru probíhá až při žádosti o vyhodnocení načteného řetězce Potíž je v tom, že soubor není možné zavřít Soubor se zavře po dočtení nakonec nebo ho uzavře GC Výhoda líného vstupu spočívá v tom, že se souborem je možné pracovat jako s obyčejným seznamem (což je velice pohodlné) Některým programům navíc nevadí, že soubor není uzavřen ihned 16. dubna 2014 41
Děkuji za pozornost!