Wie kann ich Implizite in Scala verketten?

77

Das pimp-my-library-Muster ermöglicht es mir, einer Klasse scheinbar eine Methode hinzuzufügen, indem ich eine implizite Konvertierung von dieser Klasse in eine Klasse zur Verfügung stelle, die die Methode implementiert.

Scala erlaubt jedoch nicht, dass zwei solche impliziten Konvertierungen stattfinden, sodass ich nicht Adazu kommen kann C, ein implizites Azu Bund ein anderes implizites Bzu zu verwenden C. Gibt es einen Weg, um diese Einschränkung zu umgehen?

Daniel C. Sobral
quelle
4
@ryeguy Hier ist eine Meta-Frage für die Zuhälter- / Anreicherungsdebatte , weil heiliger Mist diesen Tag. Dieser Tag ...
Charles

Antworten:

106

Scala hat eine Einschränkung für automatische Konvertierungen zum Hinzufügen einer Methode. Dies bedeutet, dass beim Versuch, Methoden zu finden, nicht mehr als eine Konvertierung angewendet wird. Zum Beispiel:

class A(val n: Int)
class B(val m: Int, val n: Int)
class C(val m: Int, val n: Int, val o: Int) {
  def total = m + n + o
}

// This demonstrates implicit conversion chaining restrictions
object T1 { // to make it easy to test on REPL
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB(a: A): B = new B(a.n, a.n)
  implicit def bToC(b: B): C = new C(b.m, b.n, b.m + b.n)

  // won't work
  println(5.total)
  println(new A(5).total)

  // works
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

BEARBEITEN: Ansichtsgrenzen ('<%') sind seit Scala 2.11 veraltet. Https://issues.scala-lang.org/browse/SI-7629 (Sie können stattdessen Typklassen verwenden.)

Wenn jedoch eine implizite Definition einen impliziten Parameter erfordert selbst (Ansicht gebunden), Scala wird für zusätzliche implizite Werte für aussehen so lange wie nötig. Fahren Sie mit dem letzten Beispiel fort:

// def m[A <% B](m: A) is the same thing as
// def m[A](m: A)(implicit ev: A => B)

object T2 {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB[A1 <% A](a: A1): B = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1): C = new C(b.m, b.n, b.m + b.n)

  // works
  println(5.total)
  println(new A(5).total)
  println(new B(5, 5).total)
  println(new C(5, 5, 10).total)
}

"Magie!", Könnte man sagen. Nicht so. So würde der Compiler jeden einzelnen übersetzen:

object T1Translated {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB(a: A): B = new B(a.n, a.n)
  implicit def bToC(b: B): C = new C(b.m, b.n, b.m + b.n)

  // Scala won't do this
  println(bToC(aToB(toA(5))).total)
  println(bToC(aToB(new A(5))).total)

  // Just this
  println(bToC(new B(5, 5)).total)

  // No implicits required
  println(new C(5, 5, 10).total)
}

object T2Translated {
  implicit def toA(n: Int): A = new A(n)
  implicit def aToB[A1 <% A](a: A1): B = new B(a.n, a.n)
  implicit def bToC[B1 <% B](b: B1): C = new C(b.m, b.n, b.m + b.n)

  // Scala does this
  println(bToC(5)(x => aToB(x)(y => toA(y))).total)
  println(bToC(new A(5))(x => aToB(x)(identity)).total)      
  println(bToC(new B(5, 5))(identity).total)

  // no implicits required
  println(new C(5, 5, 10).total)
}

Also, während bToCes als implizite Konvertierung verwendet wird aToBund toAals implizite Parameter übergeben wird , anstatt als implizite Konvertierungen verkettet zu werden.

BEARBEITEN

Verwandte Frage von Interesse:

Daniel C. Sobral
quelle
5
Schöne Erklärung. Der angegebene Grund für das Nichtzulassen impliziter Konvertierungsverkettungen besteht darin, Komplexität und das Debuggen von Albtraum zu vermeiden. Ich frage mich, warum dann Verkettung für implizite Parameter zulässig ist.
Adrian
3
Nett! Ich habe etwas Neues gelernt. Dies sollte sich auf der Seite "Versteckte Funktionen" befinden.
Aaron Novstrup
Vielen Dank! Ein kleines Problem: Ich musste den Funktionen explizite Ergebnistypen hinzufügen aToBund bToCin, T2als ich es in REPL versuchte.
Agl
@Agl Es wird in der kommenden Version 2.9 nicht erforderlich sein, aber ich habe den Code geändert, um ihn mit 2.8 kompatibel zu machen. Vielen Dank.
Daniel C. Sobral
1
Nur eine Notiz, die die Verkettung ist, die Sie versuchen, beinhaltet höher sortierte Typen, dann kann Sie die Typinferenz wieder auf die Palme bringen. Dh ich habe M [A]. Ich habe ein implizites A => B und ein implizites M [ ] => N [ ], wobei sowohl M als auch N monadisch sind. Ich möchte mit den beiden Konvertierungen ein N [B] erstellen. Um diese zu verketten, ist ein zusätzlicher Methodenaufruf erforderlich, der erste zum Erfassen von M [_] und der zweite zum Erfassen von A.
jsuereth
12

Beachten Sie, dass Sie auch Kreise mit impliziten Parametern erstellen können. Diese werden jedoch vom Compiler erkannt, wie dies zeigt:

class Wrap {
  class A(implicit b : B)
  class B(implicit c : C)
  class C(implicit a : A)

  implicit def c = new C
  implicit def b = new B
  implicit def a = new A
}

Die dem Benutzer gegebenen Fehler sind jedoch nicht so klar wie sie sein könnten; es beschwert sich nur could not find implicit value for parameterfür alle drei Baustellen. Dies könnte das zugrunde liegende Problem in weniger offensichtlichen Fällen verschleiern.

Raphael
quelle
1

Hier ist ein Code, der auch den Pfad akkumuliert.

import scala.language.implicitConversions

// Vertices
case class A(l: List[Char])
case class B(l: List[Char])
case class C(l: List[Char])
case class D(l: List[Char])
case class E(l: List[Char])

// Edges
implicit def ad[A1 <% A](x: A1) = D(x.l :+ 'A')
implicit def bc[B1 <% B](x: B1) = C(x.l :+ 'B')
implicit def ce[C1 <% C](x: C1) = E(x.l :+ 'C')
implicit def ea[E1 <% E](x: E1) = A(x.l :+ 'E')

def pathFrom(end:D) = end

pathFrom(B(Nil))   // res0: D = D(List(B, C, E, A))
Sagie Davidovich
quelle