Skriptování ve Scale

Scalu jako škálovatelně navržený jazyk lze používat jak pro tvorbu velkých systémů (jako čistě objektový jazyk s bohatým typovým systémem), tak pro psaní různých skriptů (díky expresivnosti a funkcionálním konstrukcím). Psaní skriptů si představíme prakticky na příkladu dumpu dat z velké databázové tabulky do více souborů, s jejichž velikostí si ještě filesystém poradí.

Spouštění skriptů

Skript je sekvencí příkazů, kterou můžeme uložit do souboru s příponou .scala a následně spustit pomocí Scala interpreteru, který bychom měli mít v systémové proměnné PATH po instalaci distribuce Scaly:

$ scala script.scala

Pokud je skript závislý na dalších knihovnách kromě knihoven Scaly, musíme je přidat do classpath:

$ scala -cp mylibrary.jar script.scala

Scala "interpreter" nejdříve kód skriptu kompiluje, nahlásí všechny případné chyby a pokud je vše v pořádku, provede spuštění zkompilovaného kódu (alternativně lze skript nejdříve zkompilovat pomocí scalac a poté spustit zkompilovanou verzi skriptu).

Při spuštění je možné předat skriptu parametry, ke kterým pak skript může přistupovat pomocí pole args, které je ve skriptu automaticky dostupné (prvním parametrem je tedy args(0)).

Příklady různých skriptů lze nalézt na webu jazyka Scala.

Scala REPL

Interpreter Scaly můžeme spustit v interaktivním režimu jako REPL (Read Eval Print Loop), kdy je každý řádek kódu/blok kódu, který napíšeme, hned vyhodnocen a informace o výsledku (např. o nově vytvořené proměnné) a jeho typu je ihned vypsána na výstup:

$ scala
scala> val name = "Martin"
name: java.lang.String = Martin
scala> println("Hello, " + name)
Hello, Martin  
scala> :quit

REPL je velmi užitečnou pomůckou pro prvotní prověřování kódu (realizovatelnosti návrhu), předtím než jej použijeme v unit testech a v produkční implementaci. REPL vypisuje řadu zajímavých informací o napsaném kódu, jeho používání vede k lepší představě o tom, jak Scala kompilátor opravdu funguje.

Dump velké databázové tabulky

Podívejme se na příklad skriptu, který se připojuje pomocí JDBC k databázi a data z velké databázové tabulky (řádově např. jednotky nebo desítky GB) exportuje v podobě SQL insertů do více souborů. Každý soubor bude obsahovat nejvýše maximální zadaný počet záznamů. Na skriptu si budeme demonstrovat některé zajímavé vlastnosti Scaly umožňující psaní expresivnějšího a bezpečnějšího kódu. Skript můžete volně použít k vlastním pokusům nebo ve svých projektech.

dumptable.scala:

Skript lze spustit z příkazové řádky (jako svou runtime závislost vyžaduje JDBC driver používané databáze):

$ scala -cp mysql-connector-java-5.1.7-bin.jar dumptable.scala

Za povšimnutí stojí několik skutečností:

Zejména funkce using:

def using[A <: { def close(): Unit }, B](resource: A)(f: A => B): B =
  try { f(resource) } finally { if (resource != null) { resource.close() } }

Funkce using je implementací tzv. Loan patternu a je příkladem použití hned několika vlastností Scaly:

  • Použití typových parametrů - funkce používá dva typové parametry A a B.
  • Použití strukturálních typů: Na první typový parametr A je kladeno speciální omezení - musí být podtypem daného typu (tzv. "upper bound" specifikovaný symbolem <:), v tomto případě strukturálního typu, který osahuje metodu close.
  • Function Currying - funkce má více seznamů parametrů - v prvním je předáván jen jeden parametr resource, v druhém také jen jeden parametr - funkce f. Při zavolání funkce using jen s prvním seznamem parametrů získáme jinou funkci, která lze volat s druhým seznamem parametrů (seznamy parametrů však můžeme specifikovat oba najednou). Druhý seznam parametrů můžeme namísto v kulatých závorkách předat ve složených závorkách, což vede při používání funkce using k pocitu, jako by se jednalo přímo o zabudovaný příkaz jazyka.
  • Funkce using je higher-order funkcí - sama přijímá jako vstupní parametr jinou funkci (f).
  • Při použití funkce using je jako druhý parametr (ve složených závorkách) zadávána anonymní funkce, která má na vstupu zdroj a vrací hodnotu, která vznikne vyhodnocením posledního příkazu anonymní funkce.

Kód skriptu je poměrně expresivní - nemusíme uvádět typy u všech proměnných, kompilátor si je dovede odvodit. Nemusí být např. uvedeny ani typy návratových hodnot funkcí, pro dobrou čitelnost kódu je však doporučované je uvádět.

Skript je napsaný funkcionálním stylem - namísto cyklů a použití proměnných, u kterých se mění stav (var), je použita překladačem optimalizovaná rekurze (tail rekurze) a všechny reference na proměnné jsou immutable (definované s klíčovým slovem val) - viz. Proč bychom se báli rekurze a Scaly. Funkcionální přístup vede k přirozenému rozpadu kódu do řady menších (často velmi krátkých) funkcí, které lze snadněji pochopit a znovupoužít.

Namísto používaní null hodnot je lepší specifikovat, že vstupní parametr nemusí být zadaný (použitím Option), což vede k explicitnějšímu zdůraznění této skutečnosti a zabránění případné časté chybě v podobě NullPointerException.

Loan pattern

Za pomoci funkce using můžeme v určitém bloku kódu používat zdroj, který je na  konci bloku automaticky a bezpečně uvolněn - jedná se o obdobu příkazu try-with-resources z Javy 7, resp. příkazu using z jazyka C#, ovšem ve Scale je stejného efektu dosaženo pouze prostředky samotného jazyka, nejedná se o žádný speciální příkaz. Funkce using přijímá zdroj (jakýkoliv objekt s metodou close) a funkci f, která má na vstupu daný zdroj a vrací případný výstup. Funkce using spustí funkci f (akci se zdrojem), která je předávána v druhém seznamu parametrů, se vstupem resource, který je předáván v prvním seznamu parametrů. Funkce f je spuštěna v chráněném bloku try-finally, přičemž ve finally dochází k uvolnění zdroje.

Pattern eliminuje nutnost psaní boilerplate kódu pro uvolňování zdroje (např. zavírání otevřeného připojení do databáze nebo souboru), a zabraňuje možným chybám, které můžeme při opakovaném psaní logiky pro uvolňování zdroje nadělat. Scala nezná checked výjimky, nenutí nás k ošetřování výjimek v případech, kdy je ošetřovat nechceme (v případě nedostupné databáze takové ošetření na úrovni vrstvy aplikační logiky zpravidla nemá smysl), to rovněž usnadňuje psaní kódu pracujícího s různými typy zdrojů.

Strukturální typy

Pomocí strukturálních typů lze nadefinovat abstraktní rozhraní typu, které nemusí být explicitně děděno ostatními typy, které mají tomuto rozhraní vyhovovat. Interně bývá ověření splnění takového kontraktu implementováno reflexí. Strukturální typ lze nadefinovat pomocí klíčového slova type s uvedením názvu strukturálního typu a definicí ve složených závorkách, nebo jako anonymní typ pouze za použití složených závorek. Uvnitř závorek se uvádí abstraktní definice členů typu:

type Resource = {
  def close(): Unit
}

Strukturální typy jsou účinným nástrojem pro omezení zbytečného boilerplate kódu a zvýšení vyjadřovací schopnosti kódu. Použití nachází typicky v případech, kdy je potřeba provést po použití objektu určitou finalizaci a není explicitně definován společný nadtyp pro objekty, které takovou finalizaci potřebují (takové objekty mohou být často různých nesourodých typů). V tomto případě umožňují strukturální typy eliminovat boilerplate kód a zároveň zamezit potenciálním chybám z opomenuté finalizace objektu (uzavření zdroje). Takové použití strukturálních typů má jasné opodstatnění.

Obecně je ale třeba se nadměrného/neopodstatněného používání strukturálních typů vyvarovat (mohou znepřehlednit kód nebo snížit výkon tím, že jsou implementovány pomocí reflexe). Pokud navrhujeme vlastní API, je vždy lepší nadefinovat a implementovat pro podobné účely explicitní, pojmenované rozhraní. To ale nemusí být možné pro třídy, které nemáme přímo pod kontrolou.

Zdroje

  1. Odersky, Martin, Lex Spoon, Bill Venners. Programming in Scala, Second Edition. Artima Press, 2010. Web: http://booksites.artima.com/programming_in_scala_2ed.
  2. Suereth, Joshua D. Scala in Depth. Manning Publications Co., 2012. Web: http://www.manning.com/suereth/.

Článek obsahuje 2 komentáře

  • v6ak

    1
    Ke skriptování ve Scale bych doporučil parametr -save, který umí
    uložit výsledek kompilátoru a příště jej použít. Jen pozor při update
    Scaly, pak je asi potřeba smazat ten jar soubor. Nevím, jestli to
    Scala udělá sama, ale asi ne.
  • Radek Beran

    2
    Díky za tip. JAR, který je výsledkem spouštění skriptu s parametrem -save, je potřeba opětovně přegenerovat nebo smazat, pokud se něco změní.