Scala type bounds vs. C# constraints on type parameters

C# má takzvané type constraint, kterými můžeme blíže specifikovat typový parametr generické třídy:
// C# class X<T> where T: ISomething
Java nic podobného nemá, což je škoda, protože generické třídy jsou velice obecné a nemůžou si nijak vynutit, aby měl typový parametr nějaké konkrétní vlastnosti.
Scala je na tom lépe než Java a type constraints obsahuje v podobě upper bound operátoru <:
.
Aniž bych si to uvědomil, dlouhou dobu jsem to používal a nepřišlo mi, že jde o nějakou extra vychytávku. Jde zkrátka o nedílnou součást typového systému, která se používá na mnoha místech ve standardní knihovně. Koncept typových omezení je ve Scale o něco obecnější a využívá kompaktnější a o něco jasnější syntaxi (tedy aspoň podle mě).
// Scala class X[T <: ISomething]
C# má dohromady 5 typů constraints, které mohou být aplikovány na třídu, interface, metodu nebo konstruktor; zkrátka na všechny deklarace, kde se můžou vyskytovat špičaté závorky. Stejně tak to platí i pro Scalu a hranaté závorky.
// C# class X<T> where T: struct
T musí být hodnotový typ, což v C# odpovídá primitivním datovým typům nebo struct
.
// Scala class X[T <: AnyVal]
Scala dokáže to samé celkem pohodlně, ale je tu několik problémů: zaprvé JVM nemá obdobu struktur a zadruhé se v tomto případě nepoužijí primitivní datové typy, ale jejich boxované verze (pokud anotace @specialized nevynutí opak). Takže body dolů a smutníky pro Scalu.
// C# class X<T> where T: class
T musí být referenční typ (v C# odpovídá třídě, interface, delegátu nebo poli).
// Scala class X[T <: AnyRef]
Ve Scale to samé: T musí být třída nebo trait.
// C# class X<T> where T: new()
T musí mít veřejný bezparametrický konstruktor.
Tohle se ve Scale nedá napodobit ani pomocí strukturálních typů, ale ani to nedává smysl. JVM na rozdíl od .NETího virtuálního stroje používá type erasure, takže potom, co kompilátor ověří správnost programu, se všechny informace o typových parametrech zahodí a v bytekódu po nich není nezůstane vůbec nic. Z toho plyne, že v generické třídě s typovým parametrem T nemůžeme během run-time vytvořit novou instanci třídy T, protože nevíme, jakou hodnotu T má.
I když někdy může být type erasure omezující, je to de facto standardní zacházení s generickými typy.
// C# class X<T> where T: BaseClass
T musí dědit z BaseClass.
// C# class X<T> where T: ISomething1, ISomething2
T musí být nebo implemetnovat rozhraní ISomething1 a ISomething2. Takhle může být uvedeno víc rozhraní a dovoleny jsou i generické.
Ve Scale se žádné překvapení nekoná:
// Scala class X[T <: BaseClass] class X[T <: Trait1 with Trait2]
// C# class X<T, U> where T: U
Tzv. „naked type constraint“: T musí být potomkem typového parametru U.
Ve Scale nepřekvapivě:
// Scala class X[T <: U, U]
Scala jde ještě o krok dál a princip typových podmínek zobecňuje. Kromě upper bound nabízí ještě lower bound a view bound.
Lower bound je přesný opak upper bound, takže například [T >: U]
– znamená, že třída T musí být U nebo předek U. Pomocí lower bound je například definována metoda cons třídy List[+A]
: def ::[B >: A] (x: B): List[B]
.
View bound [T <% U]
znamená, že typ T se dá implicitně zkonvertovat na typ U. Takže T v [T <% Int]
vyhovuje Byte, Char, Short, Int atd…