Zasoby programistyczne typu Scala
Zgodnie z to pytanie , System typów Scali to Turing kompletny . Jakie zasoby są dostępne, aby początkujący mogli skorzystać z możliwości programowania na poziomie typów?
Oto zasoby, które dotychczas znalazłem:
Te zasoby są świetne, ale czuję, że brakuje mi podstaw, a więc nie mam solidnych podstaw, na których mogę budować. Na przykład, gdzie jest wprowadzenie do definicji typu? Jakie operacje mogę wykonać na typach?
Czy są jakieś dobre materiały wprowadzające?
5 answers
Przegląd
Programowanie na poziomie typu ma wiele podobieństw do tradycyjnego programowania na poziomie wartości. Jednak w przeciwieństwie do programowania na poziomie wartości, gdzie obliczenia odbywają się w czasie wykonywania, w programowaniu na poziomie typu, obliczenia odbywają się w czasie kompilacji. Postaram się narysować paralele pomiędzy programowaniem na poziomie wartości i programowaniem na poziomie typu.
Paradygmaty
Istnieją dwa główne paradygmaty w programowaniu na poziomie typu: "zorientowane obiektowo "i"funkcjonalne". Większość przykładów związanych z tym tematem podąża za paradygmatem zorientowanym obiektowo.
[129]}dobry, dość prosty przykład programowania na poziomie typu w paradygmacie obiektowym można znaleźć w implementacji rachunku lambda apocalisp {142]}, replikowanej tutaj: {132]}// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
Jak widać na przykładzie, obiektowy paradygmat programowania na poziomie typów przebiega w następujący sposób:]}
- po pierwsze: zdefiniuj abstrakcyjną cechę z różne pola typu abstrakcyjnego (zobacz poniżej czym jest pole abstrakcyjne). Jest to szablon gwarantujący istnienie pewnych pól typu we wszystkich implementacjach bez wymuszania implementacji. W przykładzie rachunku lambda odpowiada to
trait Lambda
, które gwarantuje istnienie następujących typów:subst
,apply
, ieval
. - dalej: definiowanie podzbiorów rozszerzających cechę abstrakcyjną i implementowanie różnych pól typu abstrakcyjnego
- często te podzbiory będą parametryzowane argumentami. W przykładzie rachunku lambda podtypami są
trait App extends Lambda
, które są parametryzowane dwoma typami (S
iT
, oba muszą być podtypamiLambda
),trait Lam extends Lambda
parametryzowany jednym typem (T
) itrait X extends Lambda
(który nie jest parametryzowany). - pola typu są często implementowane przez odwoływanie się do parametrów typu subtraitu i czasami odwoływanie się do ich pól typu za pomocą operatora skrótu:
#
(który jest bardzo podobny do operatora kropki:.
dla wartości). W traitApp
W przykładzie rachunku lambda, typeval
jest zaimplementowany w następujący sposób:type eval = S#eval#apply[T]
. Jest to zasadniczo wywołanie typueval
parametru cechyS
i wywołanieapply
z parametremT
NA wyniku. Uwaga,S
ma zagwarantowany typeval
, ponieważ parametr określa go jako podtypLambda
. Podobnie, wynikeval
musi mieć Typapply
, ponieważ jest określony jako podtypLambda
, jak określono w cechy abstrakcyjnejLambda
.
- często te podzbiory będą parametryzowane argumentami. W przykładzie rachunku lambda podtypami są
Paradygmat funkcjonalny polega na definiowaniu wielu parametryzowanych konstruktorów typów, które nie są zgrupowane w cechy.
Porównanie programowania na poziomie wartości i programowania na poziomie typu
-
klasa abstrakcyjna
- poziom wartości:
abstract class C { val x }
- Typ-Poziom:
trait C { type X }
- poziom wartości:
- typy zależne od ścieżki
-
C.x
(pole odniesienia wartość / funkcja x w obiekcie C) -
C#x
(odwołanie do pola typu x w Cechie C)
-
- podpis funkcji (Brak implementacji)
- poziom wartości:
def f(x:X) : Y
W przeciwieństwie do innych typów, Typ ten nie może być używany w systemach klasycznych.]}
- poziom wartości:
- implementacja funkcji
- poziom wartości:
def f(x:X) : Y = x
- Typ-poziom:
type f[x <: X] = x
- poziom wartości:
- warunki
- sprawdzanie równości
- poziom wartości:
a:A == b:B
- Typ-Poziom:
implicitly[A =:= B]
- value-level: dzieje się w JVM poprzez test jednostkowy w trybie runtime (tzn. brak błędów runtime):
- w esensji jest twierdzenie:
assert(a == b)
- w esensji jest twierdzenie:
- type-level: dzieje się w kompilatorze poprzez typecheck (tzn. brak błędów kompilatora):
- w istocie jest porównanie typów: np.
implicitly[A =:= B]
-
A <:< B
, kompiluje tylko wtedy, gdyA
jest podtypemB
-
A =:= B
, kompiluje tylko wtedy, gdyA
jest podtypemB
iB
jest podtypemA
- Na przykład, w przypadku, gdy nie jest to możliwe, nie jest to możliwe, ponieważ nie jest to możliwe w przypadku, gdy nie jest to możliwe.]}
- przykład
- więcej porównań operatory
- w istocie jest porównanie typów: np.
- poziom wartości:
Konwersja pomiędzy typami i wartościami
-
W wielu przykładach typy definiowane za pomocą cech są często zarówno abstrakcyjne, jak i zapieczętowane, a zatem nie mogą być tworzone bezpośrednio ani przez anonimową podklasę. W związku z tym często używa się
null
jako wartości zastępczej podczas wykonywania obliczeń na poziomie wartości przy użyciu pewnego rodzaju zainteresowania:- np.
val x:A = null
, gdzieA
jest typem zależy ci na
- np.
Ze względu na wymazywanie typu, Wszystkie typy parametryzowane wyglądają tak samo. Co więcej, (jak wspomniano powyżej) wartości, z którymi pracujesz, zazwyczaj są
null
, a więc uwarunkowanie typu obiektu (np. poprzez polecenie match) jest nieskuteczne.
Sztuczka polega na użyciu ukrytych funkcji i wartości. Przypadek podstawowy jest zwykle wartością domyślną, a przypadek rekurencyjny jest zwykle funkcją domyślną. Rzeczywiście, programowanie na poziomie typu korzysta z niejawnych.
Rozważ ten przykład (wzięty z metascala i apocalisp):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
Tutaj masz kodowanie peano liczb naturalnych. Oznacza to, że masz typ dla każdej nieujemnej liczby całkowitej: specjalny typ dla 0, mianowicie _0
; a każda liczba całkowita większa od zera ma typ postaci Succ[A]
, gdzie {[47] } jest typem reprezentującym mniejszą liczbę całkowitą. Na przykład, Typ reprezentujący 2 to: Succ[Succ[_0]]
(następca zastosowany dwukrotnie do typu reprezentującego zero).
Możemy nazywać różne liczby naturalne dla wygodniejszego odniesienia. Przykład:
type _3 = Succ[Succ[Succ[_0]]]
(jest to bardzo podobne do zdefiniowania a val
jako wynik funkcji.)
Załóżmy, że chcemy zdefiniować funkcję poziomu wartości def toInt[T <: Nat](v : T)
, która przyjmuje wartość argumentu v
, która jest zgodna z Nat
i zwraca liczbę całkowitą reprezentującą liczbę naturalną zakodowaną w typie v
. Na przykład, jeśli mamy Wartość val x:_3 = null
(null
typu Succ[Succ[Succ[_0]]]
), chcemy toInt(x)
zwrócić 3
.
Aby zaimplementować toInt
, użyjemy następującej klasy:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
Jak zobaczymy poniżej, będzie obiekt zbudowany z klasy TypeToValue
dla każdego Nat
od _0
do (np.) _3
, a każdy będzie przechowywał reprezentację wartości odpowiedniego typu (tzn. TypeToValue[_0, Int]
będzie przechowywał wartość 0
, TypeToValue[Succ[_0], Int]
zapisuje wartość 1
, itd.). Uwaga, TypeToValue
jest parametryzowana przez dwa typy: T
i VT
. T
odpowiada typowi, do którego chcemy przypisać wartości (w naszym przykładzie, Nat
), A VT
odpowiada typowi wartości, do którego go przypisujemy (w naszym przykładzie, Int
).
Teraz mamy następujące dwie ukryte definicje:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
I realizujemy toInt
w następujący sposób:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Aby zrozumieć, jak działa toInt
, zastanówmy się, co robi na kilku wejściach: {132]}
val z:_0 = null
val y:Succ[_0] = null
Gdy wywołamy toInt(z)
, kompilator szuka argument implicit ttv
typu TypeToValue[_0, Int]
(ponieważ z
jest typu _0
). Znajduje obiekt _0ToInt
, wywołuje metodę getValue
tego obiektu i zwraca 0
. Ważną kwestią jest to, że nie określiliśmy programowi, którego obiektu użyć, kompilator znalazł go niejawnie.
Rozważmy teraz toInt(y)
. Tym razem kompilator szuka domyślnego argumentu ttv
typu TypeToValue[Succ[_0], Int]
(ponieważ y
jest typu Succ[_0]
). Znajduje funkcję succToInt
, która może zwracać obiekt odpowiedniego typu (TypeToValue[Succ[_0], Int]
) i ocenia go. Ta funkcja sama przyjmuje argument niejawny (v
) typu TypeToValue[_0, Int]
(to jest TypeToValue
, gdzie pierwszy parametr typu ma o jeden mniej Succ[_]
). Kompilator dostarcza _0ToInt
(jak to zostało zrobione w ocenie toInt(z)
powyżej), a succToInt
konstruuje nowy TypeToValue
obiekt o wartości 1
. Ponownie, ważne jest, aby zauważyć, że kompilator dostarcza wszystkie te wartości w sposób niejawny, ponieważ nie mamy do nich jawnego dostępu.
Sprawdzanie pracy
Istnieje kilka sposobów, aby sprawdzić, czy obliczenia na poziomie typu robią to, czego oczekujesz. Oto kilka podejść. Stwórz dwa typy A
i B
, które chcesz zweryfikować, są sobie równe. Następnie sprawdź, czy poniższa kompilacja:
-
Equal[A, B]
- z: cecha
Equal[T1 >: T2 <: T2, T2]
(zaczerpnięte z apocolisp )
- z: cecha
implicitly[A =:= B]
Alternatywnie można przekonwertować typ do wartości (jak pokazano powyżej) i wykonaj sprawdzenie wartości w trybie runtime. Np. assert(toInt(a) == toInt(b))
, gdzie a
jest typu A
, a b
jest typu B
.
Dodatkowe Zasoby
Kompletny zestaw dostępnych konstrukcji można znaleźć w sekcji Typy podręcznika referencyjnego scala (pdf) .
Adriaan Moors ma kilka prac naukowych na temat konstruktorów typów i pokrewnych tematów z przykładami z scala:
- Generics of a higher kind (pdf)
- polimorfizm konstruktora typów dla Scali: Teoria i praktyka (pdf) (rozprawa doktorska obejmująca poprzednią pracę Moorsa)
- Typ Konstruktor Polimorfizm Wnioskowanie
Apocalisp jest blogiem z wieloma przykładami programowania na poziomie typu w Scali.
- programowanie na poziomie typu w Scali jest fantastycznym oprowadzanie po programowaniu na poziomie typu, które obejmuje wartości logiczne, liczby naturalne( jak wyżej), liczby binarne, listy heterogeniczne i inne.
- More Scala Typehackery jest implementacją rachunku lambda powyżej.
ScalaZ jest bardzo aktywnym projektem, który zapewnia funkcjonalność rozszerzającą Scala API za pomocą różnych funkcji programowania na poziomie typu. Jest to bardzo ciekawy projekt, który ma duży podążam.
MetaScala jest biblioteką na poziomie typu dla Scali, zawierającą meta typy dla liczb naturalnych, booleanów, jednostek, HList itp. Jest to projekt autorstwa Jespera Nordenberga (jego bloga) {143]}.
[129]}The Michid (blog) ma kilka niesamowitych przykładów programowania na poziomie typu w Scali (z innej odpowiedzi):- Meta-Programowanie w Scali część i: dodawanie
- Meta-Programowanie ze Scali cz. II: Mnożenie
- Meta-Programowanie w Scali Część III: częściowe zastosowanie funkcji
- Meta-Programowanie w Scali: kompilacja warunkowa i rozwijanie pętli
- kodowanie poziomu typu Scala rachunku narciarskiego
W 2011 roku, po raz pierwszy w Polsce, pojawiła się nowa wersja gry.]}
(robiłem trochę badań na ten temat i oto, czego się nauczyłem. Nadal jestem w tym nowy, więc proszę zwrócić uwagę na jakiekolwiek nieścisłości w tej odpowiedzi.)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-11 16:14:19
Oprócz innych linków tutaj, są też moje wpisy na blogu na temat programowania meta na poziomie typów w Scali:
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-13 21:14:56
[[0]}zgodnie z sugestią na Twitterze: [[1]}Shapeless: an exploration of generic/politypic programming in Scala by Miles Sabin.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-13 19:28:40
- Sing jest biblioteką metaprogramowania na poziomie typu w Scali.
- początek programowania na poziomie typu w Scali
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-13 19:33:43
Scalaz posiada kod źródłowy, wiki i przykłady.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-13 19:22:58