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)

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.