Co oznaczają <: <, <% <, i =: = oznaczają w Scali 2.8 i gdzie są one udokumentowane?

201

W dokumentach API dla Predef widzę, że są one podklasami ogólnego typu funkcji (From) => To, ale to wszystko, co mówi. Co? Może gdzieś jest dokumentacja, ale wyszukiwarki nie radzą sobie z „nazwami” takimi jak „<: <”, więc nie byłem w stanie jej znaleźć.

Dalsze pytanie: kiedy powinienem używać tych funky symboli / klas i dlaczego?

Jeff
źródło
6
Oto powiązane pytanie, które może przynajmniej częściowo odpowiedzieć na twoje pytanie: stackoverflow.com/questions/2603003/operator-in-scala
Yardena
13
symbolhound.com jest twoim przyjacielem do wyszukiwania kodu :)
ron
Czy firmy Haskell typeclasswykonują zadania tych operatorów? Przykład compare :: Ord a => a -> a -> Ordering:? Próbuję zrozumieć tę koncepcję Scali w odniesieniu do jej odpowiednika w Haskell.
Kevin Meredith,

Odpowiedzi:

217

Są to tak zwane ograniczenia typu uogólnionego . Pozwalają one, w obrębie klasy lub cechy sparametryzowanej na typ, na dalsze ograniczenie jednego z jego parametrów typu. Oto przykład:

case class Foo[A](a:A) { // 'A' can be substituted with any type
    // getStringLength can only be used if this is a Foo[String]
    def getStringLength(implicit evidence: A =:= String) = a.length
}

Argument niejawny evidencejest dostarczany przez kompilator, iff Ajest String. Można myśleć o nim jako dowód , że Ajest Stringsama --the argumentem nie jest ważne, tylko wiedząc, że ona istnieje. [edytuj: cóż, technicznie jest to faktycznie ważne, ponieważ reprezentuje niejawną konwersję z Ana String, co pozwala ci dzwonić, a.lengtha kompilator nie krzyczy na ciebie]

Teraz mogę go używać w następujący sposób:

scala> Foo("blah").getStringLength
res6: Int = 4

Ale jeśli spróbuję użyć tego z Foozawartością czegoś innego niż String:

scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]

Możesz odczytać ten błąd jako „nie można znaleźć dowodów na to, że Int == String” ... tak powinno być! getStringLengthnakłada dalsze ograniczenia na rodzaj, Aniż Fooogólnie wymaga; mianowicie, możesz wywoływać tylko getStringLengthna Foo[String]. To ograniczenie jest wymuszane w czasie kompilacji, co jest fajne!

<:<i <%<działają podobnie, ale z niewielkimi zmianami:

  • A =:= B oznacza, że ​​A musi być dokładnie B
  • A <:< Boznacza, że ​​A musi być podtypem B (analogicznie do ograniczenia typu prostego<: )
  • A <%< Boznacza, że ​​A musi być widoczne jako B, prawdopodobnie poprzez niejawną konwersję (analogicznie do prostego ograniczenia typu <%)

Ten fragment kodu @retronym jest dobrym wytłumaczeniem tego, jak kiedyś tego rodzaju rzeczy były osiągane i jak ogólne ograniczenia typów ułatwiają to teraz.

UZUPEŁNIENIE

Aby odpowiedzieć na twoje pytanie uzupełniające, co prawda podany przeze mnie przykład jest dość przemyślany i nie jest oczywiście użyteczny. Ale wyobraź sobie, że używasz go do zdefiniowania czegoś takiego jak List.sumIntsmetoda, która tworzy listę liczb całkowitych. Nie chcesz zezwalać na wywoływanie tej metody na żadnym starym List, tylko List[Int]. Jednak Listkonstruktor typu nie może być tak constrainted; nadal chcesz mieć listę ciągów znaków, foos, barów i innych rzeczy. Tak więc, umieszczając uogólnione ograniczenie typu sumInts, możesz upewnić się, że tylko ta metoda ma dodatkowe ograniczenie, którego można użyć tylko w przypadku List[Int]. Zasadniczo piszesz specjalny kod dla niektórych rodzajów list.

Tom Crockett
źródło
3
No dobrze, ale są też metody o tych samych nazwach Manifest, o których nie wspomniałeś.
Daniel C. Sobral
3
Metody Manifest<:<i >:>tylko ... ponieważ OP wspomniał dokładnie o 3 odmianach uogólnionych ograniczeń typów, zakładam, że to właśnie był tym zainteresowany.
Tom Crockett
12
@IttayD: jest całkiem sprytny ... class =:=[From, To] extends From => To, co oznacza, że ​​domyślna wartość typu From =:= Tojest w rzeczywistości niejawną konwersją z Fromna To. Tak więc, akceptując niejawny parametr typu, o A =:= Stringktórym mówisz, że Amożna go niejawnie przekonwertować String. Jeśli zmienisz kolejność i sprawisz, że domyślny argument będzie typu String =:= A, nie zadziała, ponieważ byłaby to niejawna konwersja z Stringna A.
Tom Crockett,
25
Czy te trzyznakowe symbole mają nazwy? Mój problem z zupą symboliczną Scali polega na tym, że trudno jest mówić ustnie i praktycznie nie można używać Google ani żadnej innej wyszukiwarki, aby znaleźć dyskusje i przykłady ich użycia.
Gigatron
4
@Andrea Nie, zadziała to tylko wtedy, gdy typy są dokładnie takie same. Zauważ, że powiedziałem, że posiadanie niejawnej wartości typu From =:= Tow zakresie oznacza, że ​​masz niejawną konwersję From => To, ale implikacja nie działa wstecz; mającą niejawna konwersja A => Bma nie sugerować masz instancję A =:= B. =:=jest zapieczętowaną klasą abstrakcyjną zdefiniowaną w scala.Predefi ma tylko jedną publicznie ujawnioną instancję, która jest niejawna i jest typu A =:= A. Masz więc gwarancję, że domyślna wartość typu A =:= Bświadczy o tym Ai Bjest równa.
Tom Crockett,
55

Nie jest to kompletna odpowiedź (inni już na to odpowiedzieli), chciałem tylko zwrócić uwagę na następujące, które mogą pomóc lepiej zrozumieć składnię: sposób, w jaki zwykle używasz tych „operatorów”, jak na przykład w przykładzie pelotom:

def getStringLength(implicit evidence: A =:= String)

korzysta z alternatywnej składni poprawki Scali dla operatorów typów .

Tak więc A =:= Stringjest to samo co =:=[A, String](i =:=jest tylko klasą lub cechą o fantazyjnie wyglądającym imieniu). Zauważ, że ta składnia działa również z klasami „normalnymi”, na przykład możesz napisać:

val a: Tuple2[Int, String] = (1, "one")

lubię to:

val a: Int Tuple2 String = (1, "one")

Jest podobny do dwóch składni dla wywołania metody, „normalne” z .a ()i składni operatora.

Jesper
źródło
2
Potrzeby upvote ponieważ makes use of Scala's alternative infix syntax for type operators.całkowicie brakuje tego wyjaśnienia, bez którego cała sprawa nie ma sensu
Ovidiu Dolha
39

Przeczytaj pozostałe odpowiedzi, aby zrozumieć, jakie są te konstrukty. Oto, kiedy powinieneś ich użyć. Używasz ich, gdy musisz ograniczyć metodę tylko do określonych typów.

Oto przykład. Załóżmy, że chcesz zdefiniować jednorodną parę:

class Pair[T](val first: T, val second: T)

Teraz chcesz dodać metodę smaller, taką jak ta:

def smaller = if (first < second) first else second

Działa to tylko w przypadku Tzamówienia. Możesz ograniczyć całą klasę:

class Pair[T <: Ordered[T]](val first: T, val second: T)

Ale to wydaje się wstydem - klasa może mieć zastosowanie, gdy Tnie zostanie zamówiona. Z ograniczeniem typu nadal możesz zdefiniować smallermetodę:

def smaller(implicit ev: T <:< Ordered[T]) = if (first < second) first else second

Jest ok instancję, powiedzmy, Pair[File], dopóki nie dzwoń smaller na nim.

W przypadku Optionimplementatorów potrzebna była orNullmetoda, chociaż nie ma to sensu Option[Int]. Dzięki zastosowaniu ograniczenia typu wszystko jest w porządku. Możesz użyć orNullna Option[String]i możesz utworzyć Option[Int]i używać, dopóki nie zadzwonisz orNull. Jeśli spróbujesz Some(42).orNull, otrzymasz uroczą wiadomość

 error: Cannot prove that Null <:< Int
cayhorstmann
źródło
2
Zdaję sobie sprawę, że minęły lata od tej odpowiedzi, ale szukam przypadków użycia <:<i myślę, że ten Orderedprzykład nie jest już tak przekonujący, ponieważ wolałbyś raczej używać Orderingczcionki klasowej niż Orderedcechy. Coś jak: def smaller(implicit ord: Ordering[T]) = if (ord.lt(first, second)) first else second.
ebruchez,
1
@ebruchez: przypadek użycia dotyczy kodowania typów unii w niemodyfikowanej scali
17

To zależy od tego, gdzie są używane. Najczęściej używane podczas deklarowania typów parametrów niejawnych są klasami. W rzadkich przypadkach mogą być obiektami. Wreszcie mogą być operatorami Manifestobiektów. Są one zdefiniowane scala.Predefw pierwszych dwóch przypadkach, choć nie są szczególnie dobrze udokumentowane.

Mają one zapewnić sposób przetestowania relacji między klasami, podobnie jak <:i <%zrobić, w sytuacjach, gdy nie można użyć tej drugiej.

Jeśli chodzi o pytanie „kiedy powinienem ich użyć?”, Odpowiedź brzmi: nie powinieneś, chyba że wiesz, że powinieneś. :-) EDYCJA : Ok, ok, oto kilka przykładów z biblioteki. W dniu Eithermasz:

/**
  * Joins an <code>Either</code> through <code>Right</code>.
  */
 def joinRight[A1 >: A, B1 >: B, C](implicit ev: B1 <:< Either[A1, C]): Either[A1, C] = this match {
   case Left(a)  => Left(a)
   case Right(b) => b
 }

 /**
  * Joins an <code>Either</code> through <code>Left</code>.
  */
 def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]): Either[C, B1] = this match {
   case Left(a)  => a
   case Right(b) => Right(b)
 }

W dniu Optionmasz:

def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse null

Znajdziesz inne przykłady w kolekcjach.

Daniel C. Sobral
źródło
Czy to :-)jeden z nich? I zgodziłbym się, że twoja odpowiedź na „Kiedy powinienem ich użyć?” dotyczy bardzo wielu rzeczy.
Mike Miller,
„Mają zapewnić sposób na przetestowanie relacji między klasami” <- zbyt ogólny, aby był pomocny
Jeff
3
„Co do pytania„ kiedy powinienem ich użyć? ”, Odpowiedź brzmi: nie powinieneś, chyba że wiesz, że powinieneś.” <- Właśnie dlatego pytam. Chciałbym móc sam to ustalić.
Jeff