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é.
Nálepky:
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.
Super, díky za fajn článek. Pokud vás Scala zaujala, doporučuji přihlásit se na další běh kurzu https://www.coursera.org/course/progfun (ještě není vypsán termín).
Díky za připomínku, absolvoval jsem právě ten předchozí běh. Čistě pro ostatní, pokud se do něj pustíte, udělejte si i domácí úkoly, jen video prostě není dost.
Nechápu, proč je Scala tak populární. Rozumím, proč není tak rozšířené Groovy, ale co se Scaly týče, tak souhlasím s tím, co bylo napsáno v knize Seven Languages in Seven Weeks
Pokud někdo touží programovat funkcionálně, ale je nucen zůstat u JVM, proč nezvolí Clojure?
Dobrý den,
prima shrnutí, zejména ten předposlední citovaný odstavec (o Scale). Můj názor je, že kvůli větší produktivitě má smysl použít Scalu i na nové projekty, Java ssebou vleče podstatně delší historii, svoji a část z C++ a to je na výsledku trochu vidět – jak z pozitivní, tak negativní stránky.
Populární? Zatím tolik není. Zkusil jsem ráno na jednom pracovním serveru zadat „scala“ a „java“. Na scalu hledala lidi jedna firma a na javu samozřejmě hromada.
Populární jsem myslel ve srovnání s jazyky, které běží v JVM (Groovy, Clojure, Jython…)
Protože funkcionální není to samé co funkcionální. Mezi těmi rozšířenějšími jazyky pro JVM snad nenajdete dva rozdílnější jazyky než Scala a Clojure.
Protože worse is better. Scala je takové C++ v JVM světě.
Dík za článek.
f(i: Int)(implicit j:Int){ i + j}
Dobrý den, zkusím vysvětlit.
Problém je s překladam „Default“ v češtině. V anglických termínech já popisuji „Default parameter values“, Vy „Implicit parameters“.
Tady má angličtina výhody že ty pojmy tvoří, v češtine mě nic lepšího nenapadlo. Zkusím to nechat uzrát v hlavě, možná i na něco narazím. „Defaultní hodnoty parametrů“ ? Nezní česky, ale je to více odlišující. Většina jazyků „Implicit parameters“ nezná a tenhle problém nemá.
Ad 2. Ano, ta věta tam měla být, vysvětluje nejlépe.
Ad 3. Zkusíte napsat jak byste rozuměl? val je něco jako proměnná s modifikátorem
final
v Javě, var pak označuje prostě deklaraci proměnné, neboť ve Scale není třeba psát typ.Ad 4 a 5.
case classes
apattern matching
jsou důležitou součástí Scaly, leč kvůli délce jsem je ze základního článku vynechal. Narazí na to každý, pattern matching je takovýswitch
. Navíc bych pak musel popsat i extraktory a unapply().Ad 5. Monad je komplikovanější téma a nesouvisí čistě se Scalou. Psát na 3 řádky to spíš zamotá.
Díky za připomínky.
Já bych použil default = výchozí. Výchozí hodnoty parametrů.
Souhlas.
Výchozí hodnoty parametrů = Default parameter values.
Implicitní parametry = Implicit parameters.
Díky za článek, jen malá drobnost: equals a hashCode se pokud vím překrývají (override), pojem přetížení odpovídá anglickému overload a to asi není tento případ.
Jasně. Nějak jsem měl v hlavě asi ty operátory.
Překrytí = předefinování v potomkovi.
Přetížení = definice funkce stejného jména, různých parametrů.
Díky, tohle je po dlouhé době (spamy a flejmy) zase celkem dorbý článek, který mělo smysl číst.
BTW: jakou máte zkušenost s kombinováním Javy a Scaly v jednom projektu? Nemyslím jen knihovny, ale přímo kód aplikace – píšete vše ve Scale nebo část v Javě a část ve Scale?
Zatím jsem psal zdroják Scala a knihovny mix Scaly a Javy. Nejsložitější asi s využitím Spring.io. Ne že by spolupráce byla úplně bezešvá, ale funguje. Něco o spolupráci J&S je v http://www.artima.com/shop/programming_in_scala_2ed.
Scalu neznám, v PHP pragramuju pár let, po přečtění článku jsem ale nenašel nic společného s PHP a připadá mi, že pro PHP prográmátory je to příliš Cčkovský jazyk, nepřehledný (z pohledu jendnoduchého PHP) a dělat v tohle PHP aplikaci mi přijde zbytečně složité, to už se teda raději sžít s Javou.
Vždycky když slyším Scala vybaví se mi We’re Doing It All Wrong :)
https://www.youtube.com/watch?v=TS1lpKBMkgg
Asi mu rozumím, napsal jsem software v Javě o několika stovkách tisících řádků a po 10 letech vidím, co bylo všechno špatně. Ale: jak ten můj pokus tak kompilátor Scaly (a knihovny) jsou užitečný kus kódu – v případě Scaly zřejmě pro podstatně větší skupinu lidí.
Dodám alespoň jednu optimistickou přednášku:
Martin Odersky, „Working Hard to Keep It Simple“ – OSCON Java 2011 https://www.youtube.com/watch?v=3jg1AheF4n0
Dobrý den, mrzí mě že jsem Vám to nevysvětlil.
Lambda funkce, inicializátory pro List, Map a ostatní kontejnery, volitelné parametry (výchozí hodnoty parametrů), iterace přes kontejnery, možnost psát skripty, některé aspekty porovnávání objektů = tím vším je Scala podobná PHP.
Typová kontrola při všech operacích, tím se od PHP liší.
Tvrzeni, ze vyhoda scaly oproti pythonu je typova kontrola je nesmyslne. Python ma take typovou kontrolu, dokonce silnou. Autor nejspise myslel to, ze scala ma staticke typy. Jenze to neni objektivni vyhoda, to je vlastnost, ktera je subjektivni. Ja davam prednost dynamickemu typovani, je flexibilnejsi. Staticke typ jsou pro me, tedy subjektivne, nevyhodou pog. jazyka. Navic dnes ma python moznost volitelneho typehintingu, takze lze zajistit typovou kontrolu pri prekladu bez ztraty vyhod, ktere maji dynamicke typy.
Článek je mylný. Python nabízí od jakživa typovou kontrolu a na rozdíl od PHP nebo C dokonce silnou.
Autor má zřejmě na mysli statickou analýzu při kompilaci, což u nekompilovaného jazyka jaksi není. Ale existujou externí nástroje pro statickou lexikální analýzu. A dnes i s podporou volitelných datových typů.
Jeden ze současných analyzátorů se jmenuje MyPy, který nabízí velmi kvalitní statickou analýzu datových typů python kódu. Většina jazyků si o něčem takovém může nechat jen zdát. Přitom je to celé volitelná a při psaní krátkých skriptů se tím nemusí nikdo zdržovat a vymýšlet komplexní typový systém své aplikace. Ne náhodou se Python stal nejíblíbenějším jazykem na světě dle indexu TIOBE.