Zobrazují se příspěvky se štítkemScala. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemScala. Zobrazit všechny příspěvky

čtvrtek 10. ledna 2013

Grahamův problém v Javě je ukecaný. A vadí to?

Grahamův problém má ukázat, jak je nějaký jazyk expresivní. V některých jazycích stačí dva nebo tři řádky, v Javě jsem napočítal aspoň devět řádků. Vadí to?

V Javě vypadá řešení zhruba takto, když chceme být struční:

public class Accumulator {
 private int value;
 public Accumulator(int value) {
  this.value = value;
 }
 public int add(int i) {
  return value += i;
 }
}

Prakticky totéž ve Scale by bylo stručnější:

class MutableAccumulator(private var value: Int){
 def apply(i: Int) = {
  value += i
  value
 }
}

Ačkoli dám přednost Scale, musím uznat, že i Javové řešení je dobře čitelné. A jak je to s psaním? Sice nešetřím každý úder do klávesnice, ale nemám rád, když s sebou psaní kódu nese zbytečně velkou režii, která odvádí pozornost. Není to trochu problém?

Síla Javy je ale jinde. K Javě existují dobrá IDE. Napsal jsem v principu jen něco přes dva řádky, abytek řešilo celkem intuitivní cestou IDE. V mém případě IntelliJ IDEA, ale podobnou službu udělá i Eclipse nebo NetBeans. Tady je video (bez zvuku):


Pokud je to špatně vidět, doporučuji přepnout na vyšší kvalitu videa.
Nejde-li video přehrát, je ke stažení jako OGV.

Schválně jsem nastavil malé rozměry okna. Dá se na to koukat i z 3" mobilu. Až na rozměry okna ale je to pro mě normální nastavení. Nic navíc tam nezaclání, většina okna slouží k psaní kódu.

Jak vidíte na videu:

  • Mám kód, který používá neexistující (neimplementovanou) třídu a neexistující metody. IDE mě na toto upozorní.
  • Nechám tu zatím neexistující třídu vytvořit. (Alt+Enter)
  • Přidám ručně private int n;
  • Nakouknu se do mainu a vidím, že nesedí konstruktor. Nechám si tedy vygenerovat konstruktor z fieldů třídy. (Shift+Insert)
  • Nakouknu se do mainu a vidím, že ještě chybí add. Nechám si tedy vytvořit metodu add (Alt+Enter), určím návratový typ a napíšu return n += i. Ostatní jen schválím
  • V mainu teď nic není červené. Můžu spustit.
  • Ještě jako bonus přidám getter a setter. (Možná se do akumulátoru nehodí, ale pro ukázku IDE...)
  • Hmm, pojmenovat ten field n nebylo moudré. Nevadí, můžu jej přejmenovat (Shift+F6). Přejmenují se i metody getN a setN na getValue a setValue. Může se přejmenovat i parametr konstruktoru. Tady jsem to zamítl.
  • Přidám i výpis hodnoty acc.getValue(). Napsal jsem ale jen acc.gV a IDE zbytek doplnilo.

Nabízí se tu otázky. Mohou kvality IDE nahradit všechny kvality jazyka? Nebo je to naopak a dobré IDE je tu jen od toho, aby pomohlo programátorovi strpět špatný jazyk?

Pravda tu bude asi někde uprostřed. Dobré IDE těžko vyřeší šílené konstrukce nějakého jazyka. Na druhou stranu, IDE se hodí i u příjemnějších jazyků. Typickým příkladem je přejmenování nějaké metody, které je potřeba řešit i všude, kde se ta metoda používá. IDE tuto změnu udělá napříč celým projektem.

Úplně ideální kombinaci jazyka a IDE jsem nenašel. Takovou podporu IDE, jakou má Java (toto video je jen stručný nástřel), jsem u jiného jazyka neviděl. Pokud si ale můžu vybírat jazyk, dám většinou přednost Scale. Podpora IDE se sice s Javou srovnávat nemůže, ale celkem to stačí. I díky tomu, že jde o příjemnější jazyk.

pátek 4. ledna 2013

Jaké je vlastně funkcionální programování? (debata k ĚĽŠČŘ)

Jiří Knesl napsal plugin ĚĽŠČŘ, Jakub Vrána mu jej zkritizoval a Jiří Knesl zareagoval na Jakubovu kritiku tím, že mu šlo hlavně o vyzkoušení funkcionálního programování. Jakub Vrána se ale podivuje, co je na tom tak elegantního. I já se tedy do diskuze zapojím, byť se zpožděním. Napřed ale trošku odbočím.

Způsoby vyhodnocování

Nechci tu mít moc teorie, tak jen ve stručnosti:

Striktní vyhodnocování

Toto určitě všichni znáte, protože to využívají snad všechny mainstreamové jazyky. Vyhodnotí se prostě vše (nepočítáme-li optimalizace jako &&), i pokud to nebude nikdy potřeba. To je právě důvod, proč v Javascriptu výraz [3, decodeURIComponent("aa=%gg"), 8, 9][3] nevrátí hodnotu 9, ale skončí výjimkou.

Vyhodnocování on-demand

Druhá možnost je provést vyhodnocování teprve ve chvíli, kdy je to potřeba. Sem patřì normální a líné vyhodnocování, rozdíly mezi nimi teď nejsou podstatné. Pokud se v Haskellu pokusíme vyhodnotit [3, 1 `div` 0, 8, 9] !! 3, dostaneme 9, ačkoli celočíselné dělení nulou končí v Haskellu výjimkou. Funguje to díky tomu, že výraz 1 `div` 0 není potřeba vyhodnocovat. Pokud by se vyhodnocoval, vznikla by v Haskellu výjimka kvůli dělení nulou.

Asi je zřejmé, proč v mainstreamových (aspoň trošku imperativních) jazycích na toto nenarazíte: jakmile byste začali ve výrazech používat side effects, nastala by pravá magie. Těžko by se ovlivňovalo pořadí vyhodnocování (nebo to, zda by se něco vůbec vyhodnotilo) a chování programu by se začalo nepředvídatelně měnit.

Funkcionální klasika: linked list

Ve funkcionálním programování je spojový seznam velmi oblíbený. Ne, že by nešlo používat např. klasické pole, ale klasický funkcionální spojový seznam je dobré znát. Seznam je buď prázdný (v Haskellu []), nebo se skládá z jednoho prvku a reference na zbytek seznamu (v Haskellu head:tail). Tak je seznam definován rekurzivně a lze některé suffixy seznamu použí ve více různých seznamech. Například budeme mít dva seznamy o sto prvcích, ale budou se lišit vždy jen v prvním prvku, takže do paměti uložíme jen 101 prvků. Na funkčnost to nebude mí vliv, protože tyto seznamy jsou immutable. U vyhodnocování on-demand je navíc možné mít nekonečné seznamy. Součet nekonečné řady tak sice nespočítáte (aspoň ne přesně, sum (takeWhile (<epsilon) someList), tedy součet členů menších než epsilon u klesající řady, fungovat bude), ale třeba seznam všech prvočísel, ze kterého se na konec ve skutečnosti vyhodnotí jen prvních n z nich, není problém.

Jak je to s funkcemi map a filter

A už se dostávám ke článku, na který jsem reagoval. Funkce map a filter (popř. další, jako třeba reduceLeft, reduceRight, take, drop, takeWhile, dropWhile...) skutečně nejsou vše, o čem je funkcionální programování. Programovat bez nich skutečně lze. Ale není to ono. Až tak se nedivím, že se Jakub Vrána pod článkem podivil, kde je ta elegance.

Jak jsou tyto funkce (map, reduce, ...) ve skutečnosti implementovány?

Teď budu trošku kritizovat. A to článek nejen Jiřího Knesla, ale i samotné funkcionální programování. Jde mi o to, že funkce typické pro funkcionální programování není až tak snadné napsat správně. Ukážeme si to na funkci map. Nejdřív si odbydeme vyhodnocování on-demand (které zkritizuju za chvilku). Tam není nutné řešit tail call optimization, protože díky způsobu vyhodnocování nenastane rekurze v té podobě, v jaké ji známe ze striktního vyhodnocování. Můžeme tedy namapovat první prvek a rekurzivně namapovat zbytek:

map f (x:xs) = (f x) : (map f xs)

Funkce tedy vezme funkci f a seznam (x:xs). Zápis (x:xs) ve skutečnosti znamená, že parametr je neprázdný seznam a ten je rozdělen na první prvek (x) a zbytek (xs). Zbývá dořešit prázdný seznam, ten se namapuje na prázdný seznam:

map f [] = []

Jenže takto to funguje dobře jen u vyhodnocování on-demand. U striktního vyhodnocování získáme s dlouhými seznamy mnoho rekurzí a potřebujeme velký stack. Nepomůže ani tail call optimization, protože poslední operace není rekurze, ale vytvoření seznamu (:). Takto tedy ne.

Pak se nám nabízí použít akumulátor, což přesně udělal Jiří Knesl. Určitou nevýhodou je, že seznam procházíme od začátku, ale přidáváme též na začátek. Výsledný seznam tedy dostaneme v opačném pořadí:

map f list = map0 f list []
map0 f [] acc = acc
map0 f (x:xs) acc = map0 f xs ((f x):acc) -- chyba, obracíme pořadí.

Můžeme na to zavolat i funkci reverse (tu jde naštěstí napsat snadno) a dostaneme dokonce i dobrou asymptotickou složitost (konstantní na stack, lineární na heap a na čas), ale je to velmi neelegantní. O paměťové lokalitě nemluvě.

Trošku po svém si s tím poradila Scala (funkcionální jazyk se striktním vyhodnocováním). Funkce map je u třídy List efektivní, elegantní, čitelná (byť se znalostí pokročilejších konstrukcí), ale bohužel ne funkcionální. Autoři byli pragmatici, ne fanatici.

Původně jsem sem chtěl zkopírovat i implementaci pro důkaz, že to je fakt imperativně. Třída List ale nechává implementaci metody map na traitu TraversableLike, který to implementuje obecněji. Vysvětlovat tu implementaci lidem, kteří jazyk Scala nepotkali, by ale bylo nad rámec tohoto článku.

Snadný paralelismus?

Funkcionální programování k paralelismu přímo vybízí. Například ve Scale stačí do funkcionálního kódu přidat na pár míst par a hned je paralelní. (No dobře, má to svá úskalí, ale o tom třeba jindy.) U striktního vyhodnocování to funguje celkem dobře, pokud se u funkcí jako je map vzdáme části elegance. Menší problém je formát seznamu, spojový seznam není příliš vhodný u mnoha krátkých výpočtů.

Horší situace je u vyhodnocování on-demand. Co všechno má být vyhodnoceno paralelně? Pokud se výpočet něčeho odloží na později, nemusí se to vykonávat paralelně. Pokud se má vyhodnotit vše, bude se to vyhodnocovat striktně. Mimochodem, na tento problém jsem narazil ve Scale u funkce mapValues, která se kupodivu nechovala striktně. Místo toho, aby výpočet proběhl celý paralelně, se podstatná část odložila na později a spočítala se až při výpisu. Samozřejmě sériově.

Tail call optimization

A ještě jedna věc, která se sice netýká výhradně funkcionálního programování, ale – protože se tam rekurze prostě používá často – stojí za zmínku. Navíc chci v tomto reagovat i na Jiřího Knesla. Myšlenka je, že pokud je nějaké volání funkce posledním příkazem, lze uvolnit místo na stacku již při volání. Typicky se toho využívá při rekurzi. Z koncové rekurze kompilátor může udělat (mírně zjednodušeno) skok na začátek funkce. Program s rekurzí se tak může přeložit úplně stejně, jako kdyby měl cyklus. Teorie je to pěkná, Jiří Knesl se na to i odvolává při obhajobě svého skriptu. Jak je to ale doopravdy?

Hledal jsem Tail call optimization v Javascriptových enginech (na kterých bdou záviset i optimalizace Livescriptu), ale moc jsem o tom nenašel, nejvýše plány. Možná jsem jen špatně hledal, ale spíš bych se na to nespoléhal.

Ona to vlastně ani není ekvivalentní úprava. Pokud čteme stack trace (např. z výjimky), dostáváme po optimalizaci jiné údaje. To nemusí vadit, pokud o tom víme. U Javy je ale snaha zpětným nekompatibilitám (třeba i teoretickým) bránit, takže se tato optimalizace sice zvažuje, ale volání metody musí být (podle jednoho návrhu) uvozeno speciální instrukcí. Nevím o tom, že by dnes něco takového bylo v nějaké production-ready JVM podporováno. Podporovat to může kompilátor (a kompilátor pro Scalu to skutečně podporuje), ale možnosti jsou omezené. Optimalizovat takto rekurzi známou v době překladu lze relativně snadno (stačí do parametrů přiřadit nové hodnoty a goto 0, jen je potřeba řešit, aby korektně fungovalo např. i prohození parametrů), ale jiné tail calls moc dobře nejdou, protože JVM nemá instrukce pro takto low-level práci se zásobníkem. A v případě Scaly doporučuji @tailrec, aby kompilátor nezradil. U Livescriptu by teoreticky mohlo být něco podobného, překladač má asi podobné možnosti.

Závěr

Ano, funkcionální programování nabízí řadu výhod – eleganci, použitelné vyhodnocování on-demand, snadný paralelismus a další. Nemůžte ale od toho chtít všechno současně. Mám sice rád líný čistě funkcionální Haskell, ale v praxi píšu spíš v ne úplně čistě funkcionální Scale.

Pure functional programming is like anarchism: everything is stateless. Although I believe that reducing the state is often useful, I am not sure if total elimination of state is a good idea.

pondělí 5. září 2011

Dědičnost u case classes

Case classes jsou ve Scale jednoduchým nástrojem, který slouží zhruba ke stejnému účalu jako struktury v Cčku nebo entitní třídy v Javě - seskupují data, která patří k sobě. Vše je velmi jednoduché, než se sem vloží dědičnost. Pak je potřeba trochu přemýšlet, protože se situace trochu komplikuje. Ale jen trochu...


Budu postupovat po malých krocích, takže článek vypadá dlouze. Přidaný nebo změněný kód se snažím zvýrazňovat.


Terminologická odbočka

  • field - datový atribut třídy, který by měl být podle zásad zapouzdření pokud možno private. Napíšete-li v Javě ve třídě třeba private String foo = null;, vytvořili jste field.
  • vlastnost (vlastnost třídy) - data, která mají vztah ke třídě a která lze zjistit zavoláním getteru a případně nastavit zavoláním setteru. Data lze číst z fieldu (a případně zapisovat do něj), ale obecně to není nutné.

Co nám case classes nabízejí?

Když už víme, kdy zvažovat jejich použití, přejděme k tomu, co od nich dostaneme.

  • Metody pro porovnávání - equals a hashCode. Díky tomu nebudou instance té třídy ve výchozím stavu porovnávány podle identity, ale podle ekvivalence jejich členů.
  • Parametry třídy jsou defaultně veřejně viditelné, takže nemusíte psát val. (Generují se fieldy a gettery k nim.)
  • Metodu toString pro převod na čitelný řetězec.
  • Metodu apply ("statickou" metodu) pro konstrukci objektu bez klíčového slova new.
  • Metodu unapply ("statickou" metodu) pro pattern matching

Podle článku o case classes z FAQ by to mělo být již vše.

Úkol

Určeme si nějaký úkol s dědičností. Navrhuji použít elipsu a kružnici (jako její speciální případ). Každý tvar bude mít i barvu. (Možná budu za tu barvu kritizován, ale zkuste navrhnout příklad, který je co nejjednodušší, nejnázornější a nemá znaky něčeho umělého...)

Nápad s case classes

Možná vás jako první napadne toto:

package com.v6ak.example.geometry

case class Shape(color: String)

case class Ellipse(color: String, a: Int, b: Int) extends Shape(color)

case class Circle(color: String, r: Int) extends Ellipse(color, r, r)

Zkuste si to. Kompilátor řve? No dobře, tudy cesta asi nevede.

Problém

Kompilátor nám vlastně řval, že se snažíme znovu přidávat vlastnost (vlastně jen getter), která již existuje díky rodiči. A je plně implementovaná. (Onen getter není v rodiči abstraktní.) Což o to, přidáním override val k vlastnosti color bychom kompilátor umlčeli. (On by stále řval, že se to tak dělat nemá, ale bylo by to jen varování.)

U Shape jsme definovali i to, jak se barva získává. V Javě by tomu totiž odpovídal privátní field s getterem. Možná si myslíte, že budou následovat nějaké "kecy o zapouzdření". To ne, u kružnice a elipsy máme zajímavější situaci. Elipsa má hlavní a vedlejší poloosu a ve speciálním případě (pokud si jsou obě poloosy rovny) jde o kružnici. U kružnice ale máme tři vlastnosti, které nesou tu stejnou hodnotu - jednou jako hlavní poloosu, podruhé jako vedlejší poloosu a potřetí jako poloměr. Jedna hodnota by tedy byla v jedné instanci kružnice uložena třikrát! To vypadá už trošku divně, ne? No ano, ale když jsme si takto implementovali elipsu, tak to jinak nepůjde. Elipsa prostě je implementovaná tak, že má dva fieldy a k nim dva gettery pro hlavní a vedlejší poloosu. A kružnice k nim takto přidává třetí. Fieldy odebírat prostě nejdou. A i kdyby to šlo, byla by to celkem úzká vazba na implementaci v rodičovi.

Varianta s abstraktními třídami

Můžeme to ale řešit jinak:

  • Každá case class bude finální.
  • Všichni předci case classes budou abstraktní. (Pro úplnost: třídu AnyRef teď vynechávám.)
  • Díky tomu se nám nestane, že by jedna case class dědila z druhé. Třídy si tedy nebudou lézt do zelí (fieldů).

První verze

Tato verze je záměrně velmi podobná verzi plné case classes a přízpůsobil jsem tomu i styl odsazování.

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

No, už to vypadá lépe a kompilátor to vezme. Kružnice má jen jeden field pro poloměr, ne tři stejné. Ale co elipsa? Jak vytvořím elipsu? Asi to chce ještě něco trošku přidat.

Přidáváme konkrétní elipsu

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

Teď již není problém vytvořit konkrétní elipsu. Používá se ale "škaredé" GeneralEllipse, které "ční" ven. Třídu radši skryjeme a vytvoříme jí továrnu.

Skrýváme GeneralEllipse

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = GeneralEllipse(color, a, b)

}

Už je to celkem použitelné, ale pořád se dá vylepšovat. Ellipse nemá pattern matching.

Doplňujeme pattern matching

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = GeneralEllipse(color, a, b)
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

Toto si už zaslouží menší komentář. Jednodušší verze by nebyla odolná null hodnotám:

def unapply(e: Ellipse) = Some((e.color, e.a, e.b)) // vrací přímo uspořádanou trojici, ale selže u null hodnot

Proto pomocí Option(e) konvertujeme na Option, což je vpodstatě kolekce o nejvýše jednom prvku. Pro null dostáváme prázdnou kolekci (tj. None), pro jinou hodnotu dostáváme kolekci o právě jednom prvku (tj. Some(e)). A nakonec pomocí metody map konvertujeme případný obsah na uspořádanou trojici. Pro null tedy dostáváme None, protože to není elipsa, pro ostatní dostáváme rozloženou elipsu na uspořádanou trojici prvků, že kterých byla vytvořena. Narozdíl od GeneralEllipse toto funguje i pro kružnice.

K čemu to unapply využijeme?

Zde trošku odbočím. Možná si říkáte, k čemu je tu to podivínské unapply. Díky němu totiž můžete psát toto:

shape match {
 case Circle(c, r) => "kružnice o poloměru "+r
 case Ellipse(c, a, b) => "elipsa (...)"
 case _ => "neznámé"
}

Možná to nepotřebujete, pak to můžete vynechat.

Vylaďujeme detaily (někdy podstatné)

Každá kružnice je Circle

Pokud nyní vytvoříme Ellipse("yellow", 5, 5), dostaneme tím něco jiného než Circle("yellow", 5). Sice se to bude chovat podobně, ale podle == si rovny nebudou. (Metoda/"operátor" == porovnává podle metody equals a řeší null hodnoty.) Navíc Ellipse(5, 5) nebude instancí třídy Circle. Proto trošku upravíme továrnu:

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = if(a == b){
  Circle(color, a)
 }else{
  GeneralEllipse(color, a, b)
 }
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

Stylystická: Použijeme match místo podmínek

Toto je otázka spíše stylu, ale přijde mi hezčí použít match než if. Fungovat by to mělo naprosto stejně.

package com.v6ak.example.geometry

abstract class Shape{def color: String}

abstract class Ellipse extends Shape {def a: Int; def b: Int}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = a match {
  case `b` => Circle(color, a)
  case _ => GeneralEllipse(color, a, b)
 }
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

Uzavíráme okruh tříd

Můžeme chtít, aby nám Shape neimplementoval někdo jiný. První možnost je dát jeho konstruktoru viditelnost jen pro balíček, ve kterém je:

abstract class Shape private[geometry]() {def color: String}

Máme tu ale možnost vše mít v jednom souboru a použít modifikátor sealed. Díky tomu nebude možné dělat potomky Shape v jiném souboru a navíc dostaneme u pattern matchingu varování při nevhodném použití. Modifikátor dostanou všechny veřejné abstraktní třídy.

package com.v6ak.example.geometry

abstract sealed class Shape{def color: String}

abstract sealed class Ellipse extends Shape {def a: Int; def b: Int}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = a match {
  case `b` => Circle(color, a)
  case _ => GeneralEllipse(color, a, b)
 }
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

Vylepšujeme toString u elips

Pokud nechceme při převodu elipsy na řetězec dostávat GeneralEllipse(...), uvedeme, co chceme místo toho:

package com.v6ak.example.geometry

abstract sealed class Shape{def color: String}

abstract sealed class Ellipse extends Shape {
 def a: Int
 def b: Int
}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse{
 override def productPrefix = "Ellipse"
}

final case class Circle(color: String, r: Int) extends Ellipse {def a=r; def b=r}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = a match {
  case `b` => Circle(color, a)
  case _ => GeneralEllipse(color, a, b)
 }
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

A nakonec trosku whitespace

Při prvních úpravách jsem pro pocit podobnosti použil trošku nekonvenční pravidla pro odsazování apod., tak to teď napravím.

package com.v6ak.example.geometry

abstract sealed class Shape{
 def color: String
}

abstract sealed class Ellipse extends Shape {
 def a: Int
 def b: Int
}

private[geometry] final case class GeneralEllipse(color: String, a: Int, b: Int) extends Ellipse{
 override def productPrefix = "Ellipse"
}

final case class Circle(color: String, r: Int) extends Ellipse {
 def a=r
 def b=r
}

object Ellipse {

 def apply(color: String, a: Int, b: Int) = a match {
  case `b` => Circle(color, a)
  case _ => GeneralEllipse(color, a, b)
 }
 
 def unapply(e: Ellipse) = Option(e).map(e => (e.color, e.a, e.b))

}

Vyzkoušíme si

Nedělali jsme unit testy, ale aspoň si napíšeme jednoduchý kód, který demonstruje funkčnost:

package com.v6ak.example.geometry

object Demo extends Application{ // use App instead of Application in Scala <= 2.9
 val a = Circle("yellow", 5)
 val b = Ellipse("yellow", 5, 5)
 val c = Ellipse("yellow", 4, 5)
 println(a + " is equal to " + b + ": " + (a == b) + " (should be true)")
 println(a + " is equal to " + c + ": " + (a == c) + " (should be false)")
}

Nyní můžeme uložit poslední verzi tříd třeba do classes.scala a tuto ukázku do demo.scala, zkompilovat příkazem fsc *.scala a spustit příkazem scala com.v6ak.example.geometry.Demo. Měli bychom dostat toto:

Circle(yellow,5) is equal to Circle(yellow,5): true (should be true)
Circle(yellow,5) is equal to Ellipse(yellow,4,5): false (should be false)

Konečnou verzi zdrojáků spolu s demem si můžete stáhnout z Githubu,