V čem je Scala jiná než Java (a PHP)

Jakub Vrána napsal minulý týden „V čem je PHP navrženo lépe než Java“. Tento článek vznikl jako komplement a uvádí některé důvody, proč bych dal Scale přednost před Javou nebo PHP. Je to nefér porovnání, neboť Scala zřejmě vznikla i na základě toho, co autor považoval na Javě za nevhodně vyřešené.

Scala je relativně nový programovací jazyk, který dává možnost výběru mezi funkcionálním (doporučovaným) a procedurálním (Java – kompatibilním) způsobem programování. Scala je jazyk pro JVM, lze v něm tedy používat jak Java, tak Scala knihovny. Oproti Javě nabízí několik více či méně důležitých aspektů, proč stojí za zvážení.

Ve Scale nemusím psát new

Tedy alespoň někdy ne. Je je možné díky existenci metody apply() u objektů (companion objektů). Oceňuji to zejména u kontejnerů:

scala> List(Set(1,2,3), Set(3,4,5))
res8: List[scala.collection.immutable.Set[Int]] = List(Set(1, 2, 3), Set(3, 4, 5))

Je to o poznání méně balastu.

Implicitní hodnoty parametrů

scala> def f(msg:String = "Hello world") = println(msg)
f: (msg: String)Unit

scala> f()
Hello world

scala> f("Hallo")
Hallo

V Javě tahle fičura chybí.

Anonymní funkce

Ano, jistě že existují, a to od začátku. Vypadá to nějak takto:

scala> List(1,2,3).map((x) => x * 2)    // "upovídaný" zápis
res1: List[Int] = List(2, 4, 6)

scala> List(1,2,3).map(_ * 2) // a méně upovídaný zápis
res2: List[Int] = List(2, 4, 6)

Předpokládám, že Java 8 a její anonymní funkce toho poměrně dost změní. Ostatně i Scala se bude měnit s ohledem na podporu, která v Javě 8 je.

Důležité je, že pořád máme typovou kontrolu.

Přátelská gramatika jazyka

Co tím myslím? Například pokud je parametrem funkce, lze ji psát do kulatých i složených závorek:

scala> List(1,2,3).map{
     |     (x) =>
     |         x + 1
     | }
res1: List[Int] = List(2, 3, 4)

Podle mě autor (Martin Odersky) myslel při návrhu jazyka i na programátory – funkce vypadá jako blok, nikoli jako argument.

Inicializace map a ostatních kontejnerů, včetně typové kontroly

scala> Map(1->2, 2->3, 4->5)
res23: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 2 -> 3, 4 -> 5)

scala> List(1,2,3,4,5)
res24: List[Int] = List(1, 2, 3, 4, 5)

scala> Array(1,2,3, 5, 6, 7)              // i pole je kontejner
res1: Array[Int] = Array(1, 2, 3, 5, 6, 7)

Nezdá se vám to jako luxus? A je to proti PHP jen pár znaků navíc (jméno kontejneru).

Hodnota null

Hodnota null je sama o sobě problematická, program pak rád padá na NullPointerException.

Scala využívá typ Option[T], který je přítomen poměrně dlouho. Ten má dvě podtřídy:

class Some[T]
class None
scala> val c = Some(true)    // tady je uložená hodnota
c: Some[Boolean] = Some(true)

scala> val d = None          // a tady není - něco jako "v procedurální formě" by zde bylo null
d: None.type = None

Není to jazykem vynuceno, tudíž do proměnné typu Option lze přiřadit null – ale typ by vás měl varovat, abyste to nedělali.

Override

Java kdysi, ve verzi 1.5, zavedla anotaci @Override. Což je sice dobře, ale z důvodu kompatibility není tato anotace při přetěžování vyžadována. Scala má klíčové slovo override, které musí být uvedeno, pokud je standardní (ne abstraktní) metoda přetěžována.

Iterace

Tady proceduralista narazí. Samozřejmě lze použít cokoli, co vypadá, alepoň částečně, jako Java:

scala> val c = Map(1 -> 2, 2->3, 3->4)
c: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 2 -> 3, 3 -> 4)

scala> for (x <- c) println(x)
(1,2)
(2,3)
(3,4)

nebo funkcionální:

scala> c.foreach(println)
(1,2)
(2,3)
(3,4)

Přetypování a typová kontrola

Java je opravdu až zbytečně typově upovídaná. Naproti tomu mi, alespoň u větších projektů, nevyhovuje typová bezkontrola PHP (pročpak asi vznikl Hack?) nebo Pythonu (vím že oba mají obezličky typu assert(… isinstance …)).

Scala se k tomu staví jinak. Typy má, ale nenutí nás je vždy psát, prostě je odhadne:

scala> val c = Map(1 -> List(Set(1,2,3), Set(4,5,6)))
c: scala.collection.immutable.Map[Int,List[scala.collection.immutable.Set[Int]]] = Map(1 -> List(Set(1, 2, 3), Set(4, 5, 6)))

A rovnou vám řekne, co to vlastně vzniklo. Stejně tak napoví IDE, například Eclipse, pokud najedete kurzorem nad jméno proměnné.

Samozřejmě, někdy je nutné typ deklarovat. Scala občas typ odhadne jinak než chceme (ne interface nebo předka objektu, ale konkrétní třídu na konci hiearchie). U rekurzivních funkcí také musí být návratový typ uveden.

A občas je užitečné typy deklarovat. Je proměnná opravdu to, co jsme mysleli?

Mimochodem, přetypování se realizuje ve Scala metodou asInstanceOf[T], ale nedělejte to. Existují lepší věci, jako pattern matching.

val (a var)

Jak jste si jistě všimli, v příkladech používám u proměných val. val je konstanta, lépe řečeno konstatní reference, var je „normální“ proměnná.

To se samozřejmě týká právě té reference, samotnou proměnnou měnit lze (pokud není nezměnitelná, immutable).

Nemusím se upsat

Scala je na psaní kódu nenáročná:

scala> trait Similarity {         // z tutorialu Scaly, trait je neco jako interface v Jave, ale muze definovat i tela metod
  def isSimilar(x: Any): Boolean
  def isNotSimilar(x: Any): Boolean = !isSimilar(x)
}

scala> trait M                    // takhle se definuje prazdny interface
defined trait M

scala> (1 to 100).sum             // definuje par uzitecnych i zbytecnych metod
res29: Int = 5050

scala> List(1,2,3).map(_ + 1).map(_ * 2).filter((x) => x > 3)  // funkcionalni volani lze retezit (ale vseho s mirou)
res6: List[Int] = List(4, 6, 8)

Implicitní konverze ve Scale

scala> class Rational(val n:Int, val d:Int) {          // parametry patri primo tride (default konstruktor)
     | def * (that: Rational): Rational = new Rational(n * that.n, d * that.d)
     | override def toString() = n + " / " + d 
     | }
defined class Rational

scala> val r = new Rational(1,3)           // definuji si konstantu
r: Rational = 1 / 3

scala> r * 6                               // nemuzu nasobit cislem, tu operaci nemam definovanou
:15: error: type mismatch;
 found   : Int(6)
 required: Rational
              r * 6
                  ^

scala> implicit def IntToRational(v:Int) = new Rational(v, 1)   // ale definuji si konverzi z Int na Rational
IntToRational: (v: Int)Rational

scala> r * 6                          // a kompilator ji pouzije
res33: Rational = 6 / 3

Aby implicitní konverze fungovala a nedělala více špatného než pomáhala, musí být konverzní funkce dostupná „přes 1 identifikátor“ a označená implicit (což je pravidlo Scaly). Tedy definována ve stejném zdrojovém souboru nebo importována tak, aby byla použitelná bez jména balíku (bez tečky v identifikátoru).

Přetížení „operátorů“

První věc, Scala operátory nemá, všechno jsou metody. A ty lze samozřejmě přetěžovat. Jako C++ programátorovi mi to u Javy chybělo, byť se musí používat rozumně.

Immutable

Scala je funkcionální jakyk, dává přednost immutable (nezměnitelným) objektům. Ono to může působit neefektivně, vezměme si:

scala> val c = Map(1->2, 3 -> 5, 7 -> 9)
c: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 5, 7 -> 9)

scala> val d = c + (1 -> 6)   // vyrobi dalsi Mapu
d: scala.collection.immutable.Map[Int,Int] = Map(1 -> 6, 3 -> 5, 7 -> 9)

Ale proč to?

Pro klidný spánek nás, programátorů. Pokud někam předáte nezměnitelný objekt (typicky do jiného threadu), tak víte, že dorazí přesně v té podobě, co jste ho odeslali. Určitě ho nějaký další thread nezmění v nejhorší možnou chvíli.

Za druhé, pokud píšete vícevláknovou aplikaci, objekty, které nelze modifikovat, přeci není potřeba zamykat.

Za třetí, může to šetřit paměť. Pokud se datová struktura nemůže měnit, lze například u binárního stromu část struktury sdílet (vytvořím nový strom, kde je polovina dat sdílena s tím starým).

Samozřejmě, existuje i hiearchie měnitelných kontejnerů (scala.collection.mutable).

Kompilace

Scala se samozřejmě kompiluje. Kompilátor se, nepřekvapivě, jmenuje scalac. Ve scale lze ale i psát skripty:

// soubor hw.scala
object HelloWorld {
	def main(args:Array[String]): Unit = println("Hello world!")
}

// a příkazová řádka
[profa@cobra4 ~]$ scala hw.scala 
Hello world!

Jak vidíte, scala si program interně zkompilovala a následně spustila.

Porovnávání ve Scale    ̶ čehokoli, nikoli jen řetězců

Tady je scala podobná Javě, ale zároveň v něčem jiná. Výsledek == je dán interním použitím metody equals, nejde o porovnání referencí jako v Javě.
Výsledek je pak:

scala> val c = "Hello"
c: String = Hello

scala> val d1 = c + " world"
d1: String = Hello world

scala> val d2 = c + " world"
d2: String = Hello world

scala> d1 == d2                // hodnoty řetězců se rovnají
res14: Boolean = true

scala> d1 eq d2
res15: Boolean = false         // ale jejich reference nikoli

Ve Scale se to zdá o trochu více intuitivní. Tak jako tak, equals (a hashCode) musíte pro své objekty přetížit, jinak se budou v equals porovnávat reference, což obvykle nechceme.

Přetížení equals pro String je samozřejmě uděláno již ve standardní knihovně.

tail-rekurzivita

Mějme jednoduchý příklad na faktoriál:

scala> def f(n:Int):Int = if (n <= 1) 1 else n * f(n - 1)
f: (n: Int)Int

scala> f(3)
res42: Int = 6

pokud ho přepíšeme takto:

scala> def f(n:Int):Int = {
     |    def facc(n:Int, acc:Int):Int = if (n <= 1) acc else facc(n - 1, n * acc)
     |    facc(n, 1)
     | }
f: (n: Int)Int

scala> f(3)
res3: Int = 6

použije kompilátor optimalizaci, při které jenom prohodí proměnné na zásobníku, bez vytváření další úrovně vnoření – funkce běží ve všech cyklech v tom samém stack framu.

Šetří to zásobník (toho nemusí být vůbec dost) a „vracení se“ po zásobníku, kdy se jen předává pro všechny úrovně vnoření návratová hodnota.
Takový cyklus nevyrobí StackOverflowException, a to i když rekurzivních volání bude opravdu hodně.

Ve Scale dokonce existuje anotace @tailrec, kdy kompilátor zkontroluje, jestli opravdu používá tail-rekurzivní volání a odmítne kompilaci, pokud to tak není.

scala> @tailrec def f(n:Int):Int = if (n <= 1) 1 else n * f(n  - 1)
:9: error: could not optimize @tailrec annotated method f: it contains a recursive call not in tail position       @tailrec def f(n:Int):Int = if (n <= 1) 1 else n * f(n - 1)

Asi si na první pohled řeknete, že si někdo při vymýšlení tohohle něčeho přihnul, ale rekurzivita je pro mnoho datových struktur (stromy nebo i seznamy) ve Scale a pro paralelní programování přirozená.

Lazy, i programovací jazyk může být líný

Tady budou doma programátoři funkcionálních jazyků, kde je lazy-evaluation „1st class citizen“.

Asi nejlepší bude příklad:

scala> lazy val c = { println("c eval"); 1 }
c: Int = <lazy>

scala> lazy val d = c
d: Int = <lazy>

scala> d              // hodnota "d" se spocitala az tady, kdyz jsem si o ni rekl
c eval
res16: Int = 1

Vypadá to jako hříčka, ale vezměme si Stream (jako List, seznam z jiných jazyků), který je vyhodnocován „líně“.

Ve Scale to dává příjemnou možnost vyrobit nekonečnou sekvenci:

scala> val s:Stream[Int] = Stream.cons(0, Stream.cons(s.head + 1, s))     // generuje nekonečnou sekvenci 0 a 1
s: Stream[Int] = Stream(0, ?)

scala> s.take(10).toList                                                  // a zajima nas prvnich 10 prvku (.toList je trik, aby se vysledek zobrazil)
res21: List[Int] = List(0, 1, 0, 1, 0, 1, 0, 1, 0, 1)

Použití? Například pro prohledávání stavového prostoru nějaké množiny řešení. Prakticky ovšem použijeme spíše funkci:

scala> def iter(acc:Int, step:Int): Stream[Int] =
     | Stream.cons(acc + step, iter(acc + step, step))
iter: (acc: Int, step: Int)Stream[Int]

scala> iter(1, 3).take(5).toList
res22: List[Int] = List(4, 7, 10, 13, 16)

Závěr?

Závěr si udělejte prosím sami. Podle mne, i pokud nebudete využívat funkcionální části Scaly, stojí ten zbytek za to. Oproti PHP a Pythonu nabízí typovou kontrolu.

Nedávno se mě u pohovoru ptali, proč si myslím, že Scala není tak rozšířená jako Java. Myslím, že jedna věc je, že za ní nestojí Sun (nebo Oracle), a druhá věc je, že k jejímu plnému využití je potřeba přehodit cosi v hlavě. Ono hodně z toho, co se ve Scale naučíte, lze využít i jinde.

Autor má raději jazyky s typovou kontrolou než PHP.  Minulé roky strávil na tvorbě software pro jeden z českých e-shopů.

Komentáře: 20

Přehled komentářů

Tomas Dvorak Pěkné srovnání, díky
David Šauer Re: Pěkné srovnání, díky
Luboš Račanský Nechápu, proč je Scala tak populární
David Šauer Re: Nechápu, proč je Scala tak populární
Luboš Račanský Re: Nechápu, proč je Scala tak populární
Ladislav Thon Re: Nechápu, proč je Scala tak populární
Ladislav Thon Re: Nechápu, proč je Scala tak populární
razor par poznamek
David Šauer Re: par poznamek
vaclav.sir Re: par poznamek
David Šauer Re: par poznamek
tomaszalusky přetížení x překrytí
David Šauer Re: přetížení x překrytí
Kinkos Díky
David Šauer Re: Díky
Tom nepodobnost s PHP
Jiří Scala
David Šauer Re: Scala
David Šauer
petr vyhoda?
Zdroj: https://www.zdrojak.cz/?p=12746