20.5. Konflikty w definicjach konwersji
W pliku ImplicitConversions1.scala znajdują się dwie definicje metod definiujących konwersje.
Plik ImplicitConversions1.scala: implicit def convSymbolToString1(x: Symbol):String = x.toString + "1" implicit def convSymbolToString2(x: Symbol):String = x.toString + "2"
Obie metody mają podobne sygnatury. Obie definiują konwersję z Symbol na String. Jeśli po wczytaniu pliku w konsoli spróbujemy wykonać operację wymagającą takiej konwersji, otrzymamy błąd wynikający z odnalezienia niejednoznacznych definicji konwersji.
scala> import language.implicitConversions
import language.implicitConversions
scala> :load ImplicitConversions1.scala
Loading ImplicitConversions1.scala...
convSymbolToString1: (x: Symbol)String
convSymbolToString2: (x: Symbol)String
scala> val c: String = 'abc
<console>:13: error: type mismatch;
found : Symbol
required: String
Note that implicit conversions are not applicable because they are ambiguous:
both method convSymbolToString1 of type (x: Symbol)String
and method convSymbolToString2 of type (x: Symbol)String
are possible conversion functions from Symbol to String
val c: String = 'abc
^
Sytuacja może wyglądać inaczej, jeśli konwersje zdefiniujemy w różny sposób. Scala wybiera definicję konwersji stosując zasadę, że definicje stosujące przekazanie parametru przez wartość mają pierwszeństwo nad definicjami stosującymi przekazywanie parametru przez nazwę, natomiast jeśli dwie definicje przekazują parametr w ten sam sposób, to definicja w postaci funkcji ma pierwszeństwo nad definicją w postaci metody. Definicja konwersji wykorzystująca klasę z modyfikatorem implicit jest przekształcana przez kompilator na definicję zwykłej klasy oraz metodę z modyfikatorem implicit. Taka definicja jest zatem traktowana na równi z konwersjami jawnie zdefiniowanymi za pomocą metody.
Istnieje zatem możliwość zdefinowania czterech różnych konwersji, które mogą współistnieć nie powodując konfliktu: za pomocą metody przyjmującej argument przez nazwę, funkcji przyjmującej argument przez nazwę, metody przyjmującej argument przez wartość i funkcji przyjmującej argument przez wartość (zamiast metod można wykorzystać klasy).
Ilustracją powyższego jest następująca sekwencja definicji oraz wyrażeń korzystających z niejawnych konwersji. Dokładanie kolejnych definicji konwersji nie powoduje powstawania błędów, gdyż kolejne definicje mają pierwszeństwo, według opisanych wyżej reguł, nad wcześniejszymi definicjami.
scala> implicit def convVersion1(x: => Float):String = x.toString + " (v.1)" convVersion1: (x: => Float)String scala> val c: String = 2.3f c: String = 2.3 (v.1) scala> def convVersion2Method(x: => Float):String = x.toString + " (v.2)" convVersion2Method: (x: => Float)String
scala> implicit val convVersion2 = convVersion2Method _ convVersion2: (=> Float) => String = <function1> scala> val c: String = 2.3f c: String = 2.3 (v.2) scala> implicit def convVersion3(x: Float):String = x.toString + " (v.3)" convVersion3: (x: Float)String scala> val c: String = 2.3f c: String = 2.3 (v.3) scala> implicit val convVersion4: (Float)=>String = _.toString + " (v.4)" convVersion4: Float => String = <function1> scala> val c: String = 2.3f c: String = 2.3 (v.4)
Ponieważ definicje konwersji wykorzystujące klasy są zamieniane przez kompilator na definicje wykorzystujące metody, próba zdefiniowania konwersji między dwoma typami dwukrotnie, raz za pomocą klasy, a raz za pomocą metody, może skutkować otrzymaniem błędu spowodowanego znalezieniem niejednoznacznych definicji konwersji.
Plik ImplicitConversions2.scala: import language.implicitConversions implicit class MkStringView(list: List[_]) { def mkString(a: Char, b: Char) = list.mkString(a.toString,b.toString,a.toString) } implicit def listToMkStringView(list: List[_]) = new MkStringView(list)
W pliku ImplicitConversions2.scala zdefiniowana jest klasa MkStringView definiująca niejawną konwersję z List[_] na MkStringView. Jednocześnie, obok niej, zdefiniowana jest konwersja między tymi samymi typami, ale za pomocą metody. Próba skorzystania z tych konwersji wywołuje błąd.
scala> :load ImplicitConversions2.scala
Loading ImplicitConversions2.scala...
import language.implicitConversions
defined class MkStringView
listToMkStringView: (list: List[_])MkStringView
scala> List(1,2,3,4).mkString('|',',')
<console>:23: error: type mismatch;
found : List[Int]
required: ?{def mkString(x$1: ? >: Char('|'),x$2: ? >: Char(',')): ?}
Note that implicit conversions are not applicable because they are ambiguous:
both method MkStringView of type (list: List[_])MkStringView
and method listToMkStringView of type (list: List[_])MkStringView
are possible conversion functions from List[Int] to ?{def mkString(x$1: ? >: Char('|'),x$2: ? >: Char(',')): ?}
List(1,2,3,4).mkString('|',',')
^
<console>:23: error: overloaded method value mkString with alternatives:
=> String <and>
(sep: String)String <and>
(start: String,sep: String,end: String)String
cannot be applied to (Char, Char)
List(1,2,3,4).mkString('|',',')
^
Konflikty pomiędzy różnymi definicjami niejawnych konwersji można rozwiązywać również w inny sposób, co ilustruje przykład z pliku ImplicitConversions3.scala.
Plik ImplicitConversions3.scala: class ImplicitConversions1 { implicit class any2either1[T](a: T) { def either: Either[T,T] = Left(a) } } object ImplicitConversions2 extends ImplicitConversions1 object ImplicitConversions3 extends ImplicitConversions1 { implicit class any2either3[T](a: T) { def either: Either[T,T] = Right(a) } }
Klasa ImplicitConversions1 zawiera oznaczoną modyfikatorem implicit klasę definiującą konwersję pozwalającą na użycie na dowolnej wartości metody either, która zwraca instancję klasy Left, typu Either[T,T]. Obiekt ImplicitConversions2 rozszerza klasę ImplicitConversions1, dzięki czemu po zaimportowaniu jego składowych konwersja staje się dostępna.
scala> :load ImplicitConversions3.scala Loading ImplicitConversions3.scala... defined class ImplicitConversions1 defined object ImplicitConversions2 defined object ImplicitConversions3 scala> import ImplicitConversions2._ import ImplicitConversions2._ scala> 1.either res1: Either[Int,Int] = Left(1)
Obiekt ImplicitConversions3 również rozszerza klasę ImplicitConversions1, dodatkowo definiując klasę any2either3, również oznaczoną modyfikatorem implicit i definiującą podobną pod względem typów konwersję, jak klasa any2either1 z tą jednak różnicą, że zamiast klasy Left jest użyta klasa Right. Mimo podobieństwa nie ma konfliktu między obiema konwersjami, gdyż jedna z nich zdefiniowana jest w nadklasie i w takim przypadku jest ona traktowana jako konwersja o niższym priorytecie. W konsekwencji, ewaluacja podobnego wyrażenia, jak we wcześniejszym przykładzie nie powoduje wystąpienia błędu.
scala> import ImplicitConversions3._ import ImplicitConversions3._ scala> 1.either res2: Either[Int,Int] = Right(1)
Plik ImplicitConversions1.scala:
implicit def convSymbolToString1(x: Symbol):String = x.toString + "1"
implicit def convSymbolToString2(x: Symbol):String = x.toString + "2"
