[Scala] Call by name oraz Function0

0
  1. Zastanawia mnie taka sytuacja :
def m() = {println("m"); 0}
def callByName(x: => Int) = {println("call by name"); x}
callByName(m())

Wynik

call by name
m

To że najpierw ewaluuję metodęm() nie ma wpływu na call by name ?
2. Czy są jakieś zasadnicze różnice jeśli do parametru typu call by name wkładam metodę :

def m() = ...

vs

def m = ...

?
3. Czy dobrze mi się wydaje, że dla typów x: => R i x: () => R ewaluacja następuje w ten sam sposób - za każdym razem gdy zostaną wezwane ?
4. Czy przekazywanie typu funkcyjnego przez parametr call by name ma jakiś sens ?

1

Nie jestem moncym scalowcem, więc warto wziąć poprawkę - @Wibowit może uzupełni.

ad 1. Formalnie to nie ma nic dziwnego. m() nie jest ewaluowane do momentu użycia. Czyli trafia jako argument (pseudozapis) callByName( => m() ) . W każdym miejscu gdzie pojawia się x będzie zrobiona ewaluacja wyrażenia x. Niby proste, ale mi też miesza się ze zwykłymi lambdami.

ad 2. Chyba tylko taka, że w przypadku prierwszym metodę możesz wywołać przez m i m(), w drugim tylko przez m. Konwencja jest taka, że m wskazuje na brak efektów ubocznych. Ale to jak kodeks u piratów - luźne wskazówki. kompilator nie zabroni Ci zrobić zupełnie odwrotnie.

ad 3. W zasadzie tak. Pierwszy jest tworem kompilatora, drugi zwykła lambdą. W użyciu łatwo pomieszać, bo się zachowują podobnie. Dla mnie lambdy są prostsze w użyciu. Wszytko jest pisane explicit. (Za to call by name jest mega mocnym tworem i pozwala ładne API zrobić - tylko trochę magiczne).

ad 4. Raczej niewielki - dodatkowe zamieszanie robimy.

2

Żeby sobie to wszystko w głowie poukładać trzeba wziąć pod uwagę kilka rzeczy.

  1. Funkcja, a metoda to co innego. Funkcja jest instancją typu. Konkretnie to typ Int => Int jest faktycznie typem Function1[Int, Int]. Widać to nawet w przypadku implementowania funkcji explicit, np IntelliJ podpowiada, by zamienić coś takiego class Xxx extends Function1[Int, Int] na class Xxx extends (Int => Int). Metodę można podnieć (lift) do funkcji za pomocą np operatora _, czyli
def method(a: Int): String = ???
val function = method _

Często operator _ jest niepotrzebny, jeśli wstawiamy nazwę metody jako parametr, który ma być funkcją, np:

def hof(function: Int => Int): Unit = ???
def method(a: Int): Int = ???
hof(method) // <- tutaj nie musimy wstawiać _

Operator _ może również posłużyć do zamiany call-by-name na Function0 tak jak tutaj:

  def m1(x: => Int): Int = m2(x _)
  def m2(x: () => Int): Int = x()
  1. Call by name to w zasadzie cukier składniowy na Function0 czyli na funkcje typu () => R gdzie R jest typem zwracanym. W bajtkodzie Javowym w przypadku stosowania call-by-name kompilator wprost tłumaczy to na Function0. Jednak składniowo są to różne byty i trzeba między nimi ręcznie konwertować. W zapisie call-by-name od Function0 różni się tym, że call-by-name nie wymaga nawiasów. Mamy więc równoważne zapisy:
def m() = {println("m"); 0}
def callByName(x: => Int) = {println("call by name"); x}
callByName(m())
def m() = {println("m"); 0}
def callFunction0(x: () => Int) = {println("call function0"); x()}
callFunction0(() => m())
  1. Funkcje bezparametrowe są czasami trikowe w zrozumieniu, bo kompilator dodaje sobie niejawne nawiasy jeśli ich brakuje. Np w poniższym kodzie:
def m(): Int = 5
val x = m // nie trzeba nawiasów, kompilator sobie sam dopisuje
println(x)

Ten ficzer jest średnio pomocny i chyba ma zostać zredukowany w kolejnych wersjach Scali.

0
Wibowit napisał(a):

Metodę można podnieć (lift) do funkcji za pomocą np operatora _

To akurat znam pod nazwą eta-epansion i działa chyba dla metod o różnej liczbie argumentów.
Poza tym moje pytanie było tutaj inne - jeśli przekazuję m() a nie m to dla mnie wygląda tak jakbym ewaluował tę metodę zanim będzie przekazana call by name. Teraz w sumie sobie przypomniałem o tym pomijaniu nawiasów, że Scala i tak sama dopisuje.

1

Argument przekazany przez nazwę jest zawsze leniwie ewaluowany, nie ma znaczenia jak wygląda. Trzeba tylko pamiętać o tym, że przekazanie przez wartość dotyczy samego argumentu, a nie np zmiennych w zasięgu na których domyka się argument przekazany przez nazwę. Np tutaj zmienna x jest najpierw zachłannie liczona (więc napis się wypisze), a dopiero potem wynik jest przekazany przez nazwę:

def m(x: => Int): Unit = ()
val t = {
  println("hello")
  5
}
m(t)

Tutaj całe ciało t jest wklejone jako argument, więc nic się nie wypisze:

def m(x: => Int: Unit = ()
m({
  println("hello")
  5
}) // nawiasy okrągłe można tutaj pominąć

1 użytkowników online, w tym zalogowanych: 0, gości: 1