2015-12-31 23 views
40

Jestem prawie pewna, że ​​czegoś tutaj brakuje, ponieważ jestem całkiem nowy w Shapeless i uczę się, ale kiedy jest technika Aux faktycznie wymagana ? Widzę, że jest używany do ujawnienia oświadczenia type przez podniesienie go do podpisu innej definicji "towarzysza" type.Dlaczego technika Aux jest wymagana do obliczeń na poziomie typu?

trait F[A] { type R; def value: R } 
object F { type Aux[A,RR] = F[A] { type R = RR } } 

ale czy nie jest to prawie równoznaczne z umieszczeniem R w sygnaturze typu F?

trait F[A,R] { def value: R } 
implicit def fint = new F[Int,Long] { val value = 1L } 
implicit def ffloat = new F[Float,Double] { val value = 2.0D } 
def f[T,R](t:T)(implicit f: F[T,R]): R = f.value 
f(100) // res4: Long = 1L 
f(100.0f) // res5: Double = 2.0 

widzę, że ścieżka typu zależne przyniesie korzyści jeśli można było ich używać w listach argumentów, ale wiemy, że nie możemy zrobić

def g[T](t:T)(implicit f: F[T], r: Blah[f.R]) ... 

zatem nadal jesteśmy zmuszeni do umieść dodatkowy parametr typu w sygnaturze g. Korzystając z techniki Aux, jesteśmy , a także wymagane do spędzenia dodatkowego czasu na pisanie towarzysza object. Z punktu widzenia użycia wygląda na tak naiwnego użytkownika, jak ja, że ​​nie ma żadnej korzyści z używania typów zależnych od ścieżki.

Jest tylko jeden przypadek, jaki mogę wymyślić, to znaczy, że przy danym obliczeniu na poziomie typu zwracany jest więcej niż jeden wynik na poziomie typu, a można użyć tylko jednego z nich.

Domyślam się, że wszystko sprowadza się do mnie z widokiem czegoś na moim prostym przykładzie.

Odpowiedz

49

Istnieją dwa odrębne pytania tutaj:

  1. Dlaczego członkowie typu Shapeless użyty zamiast parametrów typu, w niektórych przypadkach, w niektórych klasach typu?
  2. Dlaczego Shapeless zawiera aliasy typu Aux w obiektach towarzyszących tych klas?

Zacznę od drugiego pytania, ponieważ odpowiedź jest prostsza: aliasy typu Aux są całkowicie syntaktyczną wygodą. Nie musisz nigdy używać . Na przykład, załóżmy, że chcemy napisać metodę, która będzie kompilować, gdy wywołana tylko z dwoma hlists które mają taką samą długość:

import shapeless._, ops.hlist.Length 

def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit 
    al: Length.Aux[A, N], 
    bl: Length.Aux[B, N] 
) =() 

Klasa Length typ ma jeden parametr typu (dla typu HList) oraz jeden element typu (dla Nat). Składnia Length.Aux sprawia, że ​​stosunkowo łatwo odnieść się do członka Nat typu w niejawnej liście parametrów, ale jest to tylko wygoda, po to dokładnie równoważne:

def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit 
    al: Length[A] { type Out = N }, 
    bl: Length[B] { type Out = N } 
) =() 

Wersja Aux ma kilka zalet w stosunku wypisywanie udoskonalenie typu w ten sposób: jest mniej hałaśliwe i nie wymaga od nas zapamiętywania nazwy elementu typu. Są to jednak kwestie czysto ergonomiczne - aliasy Aux powodują, że nasz kod jest trochę łatwiejszy w czytaniu i pisaniu, ale nie zmieniają tego, co możemy lub nie możemy zrobić z kodem w jakikolwiek znaczący sposób.

Odpowiedź na pierwsze pytanie jest nieco bardziej złożona. W wielu przypadkach, w tym w moim sameLength, nie ma żadnej korzyści z tego, że Out jest elementem typu, a nie parametrem typu. Ponieważ Scala doesn't allow multiple implicit parameter sections, potrzebujemy N, aby być parametrem typu dla naszej metody, jeśli chcemy sprawdzić, czy dwie instancje Length mają ten sam typ Out. W tym momencie Out na Length może równie dobrze być parametrem typu (przynajmniej z naszej perspektywy jako autorzy sameLength).

W innych przypadkach możemy skorzystać z faktu, że czasami Shapeless (będę mówić o konkretnie gdzie za chwilę) używa elementów typu zamiast parametrów typu. Na przykład, załóżmy, że chcemy napisać metodę, która będzie zwracać funkcję, która zamieni określony typ klasy sprawę do internetowej HList:

def converter[A](implicit gen: Generic[A]): A => gen.Repr = a => gen.to(a) 

Teraz możemy używać go tak:

case class Foo(i: Int, s: String) 

val fooToHList = converter[Foo] 

I dostaniemy miłe Foo => Int :: String :: HNil. Jeśli Generic „s Repr zostały parametrem typu zamiast członka typu, musielibyśmy napisać coś takiego zamiast:

// Doesn't compile 
def converter[A, R](implicit gen: Generic[A, R]): A => R = a => gen.to(a) 

Scala nie obsługuje częściowego stosowania parametrów typu, więc za każdym razem nazywamy to metoda (hipotetyczny) musielibyśmy określić oba parametry typu, ponieważ chcemy, aby określić A:

val fooToHList = converter[Foo, Int :: String :: HNil] 

to sprawia, że ​​w zasadzie bezwartościowe, ponieważ cały sens był pozwolić rodzajowe maszynowy zorientować się w reprezentacji.

Ogólnie, gdy typ jest jednoznacznie określony przez inne parametry klasy typu, Shapeless spowoduje, że będzie on elementem typu, a nie parametrem typu. Każda klasa sprawy ma jedną reprezentację ogólną, więc Generic ma jeden parametr typu (dla typu klasy sprawy) i jeden element typu (dla typu reprezentacji); każdy kod HList ma jedną długość, więc ma jeden parametr i jeden element itd.

Tworzenie unikatowych typów elementów typu zamiast parametrów typu oznacza, że ​​jeśli chcemy używać ich tylko jako typów zależnych od ścieżki (tak jak w pierwszym converter powyżej), możemy, ale jeśli chcemy ich użyć tak, jakby były parametrami typu, zawsze możemy albo napisać wyrafinowanie typu (albo ładniej syntaktyczną wersję Aux). Jeśli Shapeless tworzył od początku te typy parametrów, nie byłoby możliwe przejście w przeciwnym kierunku.

Na marginesie, to zależność między typem „parametry” klasy Type'S (używam cudzysłowu, ponieważ nie może być parametry w dosłownym Scala sensie) jest nazywany "functional dependency" w językach takich jak Haskell, ale nie powinieneś czuć, że musisz zrozumieć cokolwiek na temat funkcjonalnych zależności w Haskell, aby uzyskać to, co dzieje się w Shapeless.

Powiązane problemy