Přejít k navigační liště

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

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

Články PHP, Různé

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.

Komentáře

Subscribe
Upozornit na
guest
21 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Tomas Dvorak

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).

Luboš Račanský

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

Scala represents pure heresy to pure functional programmers and pure
bliss to Java developers.

Clojure and Java desperately need each other. Lisp needs the market
place that the Java virtual machine can offer, and the Java community
needs a serious update and an injection of fun.

 

Strictly speaking, Scala is not a pure functional programming
language, just like C++ is not a pure object-oriented language.

 

When the object-oriented paradigm was new, the masses could not accept
Smalltalk because the paradigm was too new. We needed a language that
would let them continue to do procedural programming and experiment
with object-oriented ideas. With C++, the new object-oriented tricks
could live safely beside the existing C procedural features. The
result was that people could start using the new tricks in an old
context.

 

I do find Scala’s syntax to be a little academic and hard on the eyes.

 

The problem is that moving back and forth between Scala and Java will
take more effort than it should.

 

I would use Scala to improve my productivity if I had a significant
investment in Java programs or programmers. I’d also consider Scala
for an application that has significant scalability requirements that
would require concurrency. Commercially, this Frankenstein has a good
shot because it represents a bridge and fully embraces a significant
programming community.

 

Pokud někdo touží programovat funkcionálně, ale je nucen zůstat u JVM, proč nezvolí Clojure?

Clojure and Java desperately need each other. Lisp needs the market
place that the Java virtual machine can offer, and the Java community
needs a serious update and an injection of fun.

Luboš Račanský

Populární jsem myslel ve srovnání s jazyky, které běží v JVM (Groovy, Clojure, Jython…)

Ladislav Thon

Pokud někdo touží programovat funkcionálně, ale je nucen zůstat u JVM, proč nezvolí Clojure?

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.

Ladislav Thon

Nechápu, proč je Scala tak populární.

Protože worse is better. Scala je takové C++ v JVM světě.

razor

Dík za článek.

  1. Řekl bych nešťasně pojmenovaná sekce „Implicitiní hodnoty parametrů“ v případě Scala kvůli existenci implicitního hodnot (keyword implicit), což je něco jiného. f(i: Int)(implicit j:Int){ i + j}
  2. Přátelská gramatika: Složené závorky pro víceřádkový zápis. Kulaté pro jednořádkový.
  3. Pro mě trochu nesrozumitelně napsaná sekce o var a val
  4. Porovnání objektů ve Scala: možná bych zmínil case class, pro které je equals a hash „zadarmo“ od kompilátoru.
  5. Chápu, že v článku nemůže být vše. Nicméně stručný popis patterm matching a koncept Monady (např. v kontextu uvedeného Option[T]) bych tam dal ;-)
vaclav.sir

Já bych použil default = výchozí. Výchozí hodnoty parametrů.

tomaszalusky

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.

Kinkos

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?

Tom

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.

Jiří

Vždycky když slyším Scala vybaví se mi We’re Doing It All Wrong :)
https://www.youtube.com/watch?v=TS1lpKBMkgg

petr

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.

Petr

Č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.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.