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?

 100
Author: Community, 2010-12-11

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, i eval.
  • 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 i T, oba muszą być podtypami Lambda), trait Lam extends Lambda parametryzowany jednym typem (T) i trait 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 trait App W przykładzie rachunku lambda, typ eval jest zaimplementowany w następujący sposób: type eval = S#eval#apply[T]. Jest to zasadniczo wywołanie typu eval parametru cechy S i wywołanie apply z parametrem T NA wyniku. Uwaga, S ma zagwarantowany typ eval, ponieważ parametr określa go jako podtyp Lambda. Podobnie, wynik eval musi mieć Typ apply, ponieważ jest określony jako podtyp Lambda, jak określono w cechy abstrakcyjnej Lambda.

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 }
  • 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.]}
  • implementacja funkcji
    • poziom wartości: def f(x:X) : Y = x
    • Typ-poziom: type f[x <: X] = x
  • 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)
    • 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, gdy A jest podtypem B
      • A =:= B, kompiluje tylko wtedy, gdy A jest podtypem B i B jest podtypem A
      • 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

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, gdzie A jest typem zależy ci na
  • 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:

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:

Apocalisp jest blogiem z wieloma przykładami programowania na poziomie typu w Scali.

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

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

 137
Author: dsg,
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
 12
Author: michid,
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.

 6
Author: GClaramunt,
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
 5
Author: Kenji Yoshida,
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.

 4
Author: Vasil Remeniuk,
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