úvod do typeclasses ve Scale

  • Část 1: tento příspěvek
  • Část 2: typově bezpečná serializace v Scala
  • Část 3: typové zpracování chyb s vyššími typy

co je typová třída

jednou z největších překážek porozumění a používání typových tříd ve vašem vlastním kódu je samotné jméno. Na rozdíl od toho, co název evokuje, třída typu není třídou z objektově orientovaného jazyka. Podle Wikipedie:

v informatice je typová třída konstruktem typového systému, který podporuje ad hoc polymorfismus. Toho je dosaženo přidáním omezení do typových proměnných v parametricky polymorfních typech.

musíme něco rozbalit:

  • co je „ad hoc“ polymorfismus? Ať je to cokoli, to je to, co nám třídy typů pomohou dosáhnout.
  • co je to parametricky polymorfní Typ? Ať je to cokoliv, tak si vytvoříme vlastní typové třídy.

typové třídy se poprvé objevily v Haskellu a jsou základním způsobem, jak člověk vytváří abstrakce v Haskellu. Jsou tak důležité, že jsou představeny na první stránce knihy Learn You a Haskell:

třída typu je druh rozhraní, které definuje určité chování. Pokud je typ součástí třídy typu, znamená to, že podporuje a implementuje chování, které třída typu popisuje.

Ok, takže se zdá, že se snažíme definovat nějaké chování pro naše typy a implementovat toto chování odlišně v závislosti na typu. Poslední část „implementovat chování odlišně v závislosti na typu „je bit“ polymorfismu “ z dřívějšího. Takže termín začíná dávat smysl: je to třída pro typy. Třída, která implementuje chování určené třídou typu, se říká, že je členem této třídy typů- „třída typu“.

v tomto bodě by bylo vhodné říci, že to zní jako dědičnost. Oba nám umožňují vytvářet abstrakce a poskytovat polymorfismus.

proč tedy vůbec zkoumat typové třídy? V mnoha případech jsou o něco silnější a rozšiřitelnější než hierarchie dědičnosti. Mohou být mnohem jednodušší, a jsou mnohem lepší, když vaše codebase již nakloní FP přes OO. I když i v silně oo projektu, třídy typu mohou stále zachránit den.

definování typeclass

to je dost úvod-pro zbytek článku budeme zkoumat kus velmi známé funkce, která je ve skutečnosti implementována v Javě prostřednictvím dědičnosti, a implementovat ji pomocí type class: stringification.

všichni jsme obeznámeni s metodou, která existuje na všech objektech v Javě (a tedy Scala): .toString. Jeho účelem je vytvořit řetězcovou reprezentaci daného objektu. Podívejme se na to, jak bychom k tomu použili třídy typů. Haskellova verze toString se nazývá Show, takže se budeme držet zhruba tohoto jména:

trait Showable {
def show(a: A): String
}

to je ono: tady je naše Showable typová třída. Proč trvá parametr typu A? Pokud si vzpomeneme na naši první definici typových tříd, čteme, že “ dosaženo přidáním omezení do typových proměnných v parametricky polymorfních typech.“Showable je náš parametricky polymorfní typ A a je naše typová proměnná, což jednoduše znamená, že bere Typ (A) jako parametr typu. (Všimněte si, že jsme neumístili žádná omezení na A, podle definice, ale to je v pořádku. To je něco, co děláme, když skládáme třídy typů společně nebo je využíváme, což přijde později.)

implementace typeclass

nyní musíme poskytnout implementace vlastnosti Showable pro všechny typy, které chceme ukázat. Tyto implementace se nazývají „instance“ naší třídy type, protože to budou doslova instance znaku Showable. Nejčastěji se jedná o anonymní třídy, které přepisují .show, a protože se na jejich definování podílí nějaká varná deska(pamatujte na „funkční objekty“ v programování Java před lambda?), definujeme pomocníka s názvem make. Začněme implementací pro typ uživatelského jména:

object Showable {
def make(showFn: A => String): Showable = new Showable {
override def show(a: A): String = showFn(a)
}
}final case class UserName(first: String, last: String)implicit val defaultUserNameShowable = Showable.make(userName => s"${userName.first} ${userName.last}")val janeName = UserName("Jane", "Doe")

tak jak bychom to mohli vyvolat? Mohli bychom přímo volat metodu z zobrazitelné instance, jako tak:

defaultUserNameShowable.show(janeName) // "Jane Doe"

Vylepšená syntaxe

podařilo se nám vyvolat naši Showable implementaci UserName, ale nebylo to moc hezké. Museli jsme mít odkaz na naši instanci třídy typu kolem (defaultUserNameShowable) a znát její název. To je něco, co chceme abstrahovat. Pokud by příběh skončil tady, typové třídy ve Scale by nebylo tak hezké používat. Naštěstí můžeme pomocí implicitů drasticky zlepšit ergonomii. Budeme rozšiřovat Showabledoprovodný objekt pomocí pomocníka vyvolání:

implicit class ShowSyntax(a: A) extends AnyVal {
def show(implicit showable: Showable): String = showable.show(a)
}
...
janeName.show

umístěním této“ metody rozšíření “ někam do rozsahu můžeme jednoduše předstírat, že všechny objekty mají metodu .show.

čeho to dosáhlo

tak proč ne jen přepsat toString na UserName místo toho? Řekněme, že UserName žije v knihovně, na které jsme závislí, a je již sestavena v době, kdy ji konzumujeme. Nemůžeme ji podtřídit, protože se jedná o třídu final. Pomocí typových tříd však můžeme v podstatě“ připojit “ nové chování ad hoc. Toto je“ ad hoc “ část výše uvedené definice: tento polymorfismus můžeme přidat odděleně od místa, kde je definován samotný typ.

navíc, na rozdíl od dědičnosti, když definujeme typové třídy ve Scale, nemusí být „koherentní“, což znamená, že můžeme definovat více implementací pro Showable a vybrat si mezi nimi. Pro některé typové třídy je to dobrá věc, pro jiné ne tolik.

v Haskellu jsou typové třídy koherentní. Ve Scale 3 si podle Martina Oderského budeme moci pro každou typovou třídu vybrat, zda má být soudržná, nebo ne.

ale prozatím poskytněme alternativní význam show pro UserName:

implicit val secretUserNameShowable = Showable.make(_ => "<secret person>")

vše, co teď musíme udělat, je ujistit se, že pouze jedna z našich Showable instancí je v rozsahu najednou. A pokud ne, není to velký problém, dostaneme chybu „nejednoznačných implicitních hodnot“ v době kompilace:

janeName.show
> ambiguous implicit values:
both value secretUserNameShowable in object typeclasses of type => Showable
and value defaultUserNameShowable in object typeclasses of type => Showable
match expected type Showable

jedna důležitá poznámka je, že jsme nespojili UserName a Showable. Vše, co jsme udělali, je definovat význam Showable pro UserName, přičemž je udržujeme zcela nezávislí na sobě. Toto oddělení má obrovské důsledky pro udržovatelnost a pomáhá předcházet zamotaným vztahům závislostí v celém kódu.

bezpečnost kompilace

co se stane, když se pokusíme zavolat .show na objekt, pro který jsme neposkytli význam show? Jinými slovy, když neexistuje žádný příklad Showable v rozsahu pro náš cíl? Jednoduše dostaneme chybu kompilátoru:

java.util.UUID.randomUUID().show
> could not find implicit value for parameter showable: Showable

tuto chybovou zprávu můžeme skutečně přizpůsobit, když definujeme naši typovou třídu:

@scala.annotation.implicitNotFound("No Showable in scope for A = ${A}. Try importing or defining one.")
trait Showable {
...
}
...
janeName.show
> No Showable in scope for A = java.util.UUID. Try importing or defining one.

Klíčové Takeaways

  • typ třídy nám umožňují definovat sadu chování zcela odděleně od objektů a typů, které budou implementovat tato chování.
  • typové třídy jsou vyjádřeny v čisté Scale se znaky, které berou parametry typu a implikuje, aby syntaxe byla čistá.
  • typové třídy nám umožňují rozšířit nebo implementovat chování pro typy a objekty, jejichž zdroj nemůžeme upravit. (Poskytují ad hoc polymorfismus nezbytný k vyřešení problému výrazu).

co přijde dál

je pravda, že Showable je třída triviálního typu, i když ne bez jejího použití. Můžeme si snadno představit mnohem užitečnější, nicméně:

trait SafeSerializable {
def serialize(a: A): Array
def deserialize(bytes: Array): A
}
trait Convertible {
def to(a: A): B
def from(b: B): A
}
trait Functor] {
def map(a: F, f: A => B): F
}

SafeSerializable může to být skvělá alternativa k něčemu, jako je serializace Akka, kde počkáme, až za běhu, abychom zjistili, zda je Serializátor registrován pro náš typ. Je mnohem bezpečnější implicitně předat implementaci serializace v době kompilace!

Convertible by mohl být skvělý znovu použitelný způsob převodu dat napříč typy. K dispozici je knihovna Twitter se stejným cílem v mysli s názvem Bijection.

Functor je stavebním kamenem pro některé extrémně silné abstrakce v oblasti funkcionálního programování zvané „kategorické programování“. Knihovny jako ScalaZ a Cats jsou postaveny na třídách typu Functor. Jedna důležitá představa, o které jsme tentokrát nemluvili, je, že typové třídy lze snadno skládat tak, aby vytvářely stále složitější chování z jednoduchých stavebních bloků, jako je Functor.

kromě toho mnoho základních knihoven v ekosystému Scala (Play JSON, Slick, et. al) využívají typové třídy a vystavují je jako hlavní hnací sílu jejich API v mnoha případech (např.g. hrát JSON Format typ třídy). Porozumění typovým třídám je skvělý způsob, jak se stát efektivnějším při využívání toho, co je v ekosystému Scala, i když nikdy nenapíšete žádné vlastní.

v budoucnu se podívám na některé okamžitě praktické třídy typu SafeSerializable . Pro úvod do abstraktnějších tříd kategorických typů, jako jsou Functor] a Monad], nehledejte nic jiného než dokumentaci knihovny cats. Pro podrobnější průvodce programováním ve Scala pomocí typových tříd, underscore.io má fantastickou knihu na toto téma: Scala s kočkami

Část 2: typově bezpečná serializace ve Scale

podstata s kódem

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.