Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Niejawne konwersje i parametry w Scali

Ostatnio programuję trochę w Scali, więc trzeba by wziąć na tapetę coś z tego języka. Pokażę na przykładach użycie niejawnych konwersji i parametrów.

Niejawne parametry

package conversions

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

object ImplicitsDemo extends App {

  val f = Future("hello")
  println(Await.result(f, 1 second))
}

Są prostsze sposoby w Scali na wypisanie "hello" niż używanie do tego Future, ale gdyby w tego Future opakowane było jakieś bardziej wzniosłe zadanie, np. ściągnięcie tweetów z tagiem scala, to byłby to z życia wzięty przykład użycia niejawnych parametrów. Gdzie tu jest niejawny parametr? Oto sygnatura metody Future.apply:

def apply[T](body: ⇒ T)(implicit executor: ExecutionContext): Future[T]

Dzięki temu, że executor jest parametrem niejawnym, nie trzeba go podawać w Future("hello"). Nie bierze się też znikąd, kod by się nie skompilował bez:

import scala.concurrent.ExecutionContext.Implicits.global

a tam się znajduje:

implicit lazy val global: ExecutionContextExecutor = impl.ExecutionContextImpl.fromExecutor(null: Executor)

Kompilator widząc taki niejawny parametr, szuka zdefiniowanej wartości o odpowiednim typie z modyfikatorem implicit. Jest kilka rzeczy, na które trzeba uważać:

  • implicit dotyczy całej listy parametrów, zatem jeśli funkcja jest zdefiniowana:

     def f(a: String)(implicit b: Int, c: Int) = ???

    to b, c są niejawne.

  • kompilator wyszukuje wartość do wstawienia po typie, więc lepiej używać jakichś typów w miarę specyficznych dla naszego kodu - np zamiast użyć Int, albo String, lepiej opakować to we własny typ.

  • wstawiany "implicit" musi być dostępny jako pojedynczy identyfikator. Czyli, gdyby w przykładzie z Future trzeba zaimportować scala.concurrent.ExecutionContext.Implicits.global, a nie scala.concurrent.ExecutionContext.Implicits, bo kompilator nie wstawi Implicits.global.

Niejawne konwersje

Poniżej test z użyciem spec2 wzięty z szablonu "scala-test" z typesafe activators.

package specs2

import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class ListSpec extends Specification {
  
  "Calling isEmpty" should {
    "return true for a List with 0 elements" in {
      List().isEmpty must beTrue
    }
    "return false for List with > 0 elements" in {
      List(1, 2, 3).isEmpty must beFalse
    }
  }
}

String nie ma metody should, ale to działa dzięki niejawnym konwersjom, zdefiniowanym w kodzie spec2:

 implicit def described(s: String): Described = new Described(s)
 
  class Described(s: String) {
    def should(fs: =>Fragment) = addFragments(s, fs, "should")

 // reszta pominięta
  }

Kompilator widząc, że String nie ma odpowiedniej metody, szuka implicita, który może skonwertować String w typ, który taką metodę ma (tu: Described). Podobnie jest, gdy funkcja spodziewa się parametru o określonym typie, ale dostaje inny - jeśli jest zdefiniowana odpowiednia niejawna konwersja, to kompilator ją zastosuje.

Źródła:

2 komentarze :

  1. "to a, b są niejawne." Nie chodziło czasami o b i c?

    OdpowiedzUsuń
  2. racja, dzięki za zwrócenie uwagi

    OdpowiedzUsuń