Jaka jest różnica między =>, () => i Unit =>

153

Próbuję przedstawić funkcję, która nie przyjmuje argumentów i nie zwraca wartości (symuluję funkcję setTimeout w JavaScript, jeśli musisz wiedzieć).

case class Scheduled(time : Int, callback :  => Unit)

nie kompiluje się, mówiąc, że „parametry val mogą nie być wywołane przez nazwę”

case class Scheduled(time : Int, callback :  () => Unit)  

kompiluje się, ale zamiast tego musi być dziwnie wywołany

Scheduled(40, { println("x") } )

muszę to zrobić

Scheduled(40, { () => println("x") } )      

Co też działa

class Scheduled(time : Int, callback :  Unit => Unit)

ale jest przywoływany w jeszcze mniej rozsądny sposób

 Scheduled(40, { x : Unit => println("x") } )

(Jaka byłaby zmienna typu Unit?) To, czego chcę, oczywiście, to konstruktor, który można wywołać w taki sam sposób, w jaki wywołałby go, gdyby była to zwykła funkcja:

 Scheduled(40, println("x") )

Daj dziecku butelkę!

Malvolio
źródło
3
Innym sposobem użycia klas przypadków z parametrami według nazwy jest umieszczenie ich na drugorzędnej liście parametrów, np case class Scheduled(time: Int)(callback: => Unit). Działa to, ponieważ lista parametrów pomocniczych nie jest ujawniana publicznie ani nie jest uwzględniona w generowanych equals/ hashCodemetodach.
nilskp
Kilka bardziej interesujących aspektów dotyczących różnic między parametrami imiennymi a funkcjami zerowymi można znaleźć w tym pytaniu i odpowiedzi. Właściwie to właśnie tego szukałem, kiedy znalazłem to pytanie.
lex82

Odpowiedzi:

234

Call-by-Name: => Wpisz

=> TypeNotacja oznacza wywołanie według nazwy, która jest jednym z wielu sposobów, parametry mogą być przekazywane. Jeśli ich nie znasz, radzę poświęcić trochę czasu na przeczytanie tego artykułu na Wikipedii, mimo że obecnie jest to głównie wezwanie według wartości i wezwanie przez odniesienie.

Oznacza to, że to, co jest przekazywane, jest zastępowane nazwą wartości wewnątrz funkcji. Na przykład weź tę funkcję:

def f(x: => Int) = x * x

Jeśli tak to nazywam

var y = 0
f { y += 1; y }

Następnie kod zostanie wykonany w ten sposób

{ y += 1; y } * { y += 1; y }

Chociaż to podnosi punkt widzenia, co się stanie, jeśli wystąpi konflikt nazw identyfikatorów. W tradycyjnym zawołaniu według nazwy stosuje się mechanizm zwany zastępowaniem unikającym przechwytywania, aby uniknąć kolizji nazw. W Scali jest to jednak zaimplementowane w inny sposób z tym samym wynikiem - nazwy identyfikatorów wewnątrz parametru nie mogą się odwoływać ani identyfikatorów cienia w wywołanej funkcji.

Jest kilka innych punktów związanych z wezwaniem po imieniu, o których opowiem po wyjaśnieniu pozostałych dwóch.

Funkcje 0-arity: () => Typ

Składnia () => Typeoznacza typ pliku Function0. To znaczy funkcja, która nie przyjmuje parametrów i coś zwraca. Jest to równoznaczne z, powiedzmy, wywołując metodę size()- bierze żadnych parametrów i zwraca liczbę.

Ciekawe jest jednak to, że składnia ta jest bardzo podobna do składni dla literału funkcji anonimowej , co jest przyczyną pewnych nieporozumień. Na przykład,

() => println("I'm an anonymous function")

jest anonimową funkcją, literałem arity 0, której typ to

() => Unit

Moglibyśmy więc napisać:

val f: () => Unit = () => println("I'm an anonymous function")

Ważne jest jednak, aby nie mylić typu z wartością.

Jednostka => Typ

W rzeczywistości jest to tylko a Function1, którego pierwszy parametr jest typu Unit. Innymi sposobami zapisu byłyby (Unit) => Typelub Function1[Unit, Type]. Rzecz w tym, że ... jest mało prawdopodobne, że kiedykolwiek będzie to, czego się chce. Na Unitgłównym celem jest Type wskazuje na jedną wartość nie jest zainteresowany, więc nie ma sensu, aby otrzymać tę wartość.

Rozważmy na przykład

def f(x: Unit) = ...

Co można by zrobić x? Może mieć tylko jedną wartość, więc nie trzeba jej odbierać. Jednym z możliwych zastosowań byłoby łączenie funkcji zwracających Unit:

val f = (x: Unit) => println("I'm f")
val g = (x: Unit) => println("I'm g")
val h = f andThen g

Ponieważ andThenjest zdefiniowany tylko w Function1, a funkcje, które łączymy w łańcuch Unit, zwracają , musieliśmy zdefiniować je jako typu, Function1[Unit, Unit]aby móc je łączyć w łańcuch.

Źródła nieporozumień

Pierwszym źródłem nieporozumień jest myślenie, że podobieństwo między typem a literałem, które istnieje w przypadku funkcji 0-arity, istnieje również w przypadku wywołania według nazwy. Innymi słowy, myśląc tak, ponieważ

() => { println("Hi!") }

jest dosłownym przez () => Unit, a następnie

{ println("Hi!") }

byłoby dosłowne dla => Unit. Nie jest. To jest blok kodu , a nie literał.

Innym źródłem nieporozumień jest zapisanie wartości tego Unittypu , która wygląda jak lista parametrów o zerowej wartości (ale tak nie jest).()

Daniel C. Sobral
źródło
Być może będę musiał głosować jako pierwszy po dwóch latach. Ktoś zastanawia się nad składnią case => na Boże Narodzenie i nie mogę polecić tej odpowiedzi jako kanonicznej i kompletnej! Do czego zmierza świat? Może Majowie odeszli dopiero o tydzień. Czy poprawnie obliczyli lata przestępne? Oszczędność światła dziennego?
som-snytt
@ som-snytt No cóż, to pytanie nie dotyczyło case ... =>, więc nie wspomniałem. Smutne ale prawdziwe. :-)
Daniel C. Sobral
1
@Daniel C. Sobral, czy mógłbyś wyjaśnić „To jest blok kodu, a nie literał”. część. Więc jaka jest dokładna różnica między dwoma?
nish1013
2
@ nish1013 „Literał” to wartość (w niektórych przykładach liczba całkowita 1, znak 'a', ciąg znaków "abc"lub funkcja () => println("here")). Może być przekazywany jako argument, przechowywany w zmiennych itp. „Blok kodu” to składniowe rozgraniczenie instrukcji - nie jest wartością, nie można jej przekazywać, ani nic w tym stylu.
Daniel C. Sobral
1
@Alex To ta sama różnica co (Unit) => Typevs () => Type- pierwsza to a Function1[Unit, Type], a druga to a Function0[Type].
Daniel C. Sobral
36
case class Scheduled(time : Int, callback :  => Unit)

caseModyfikatora umożliwia ukryte valOUT każdy argument konstruktora. Dlatego (jak ktoś zauważył), jeśli usuniesz case, możesz użyć parametru wezwania według nazwy. Kompilator prawdopodobnie i tak by na to pozwolił, ale może zaskoczyć ludzi, gdyby utworzył val callbackzamiast przekształcać się w lazy val callback.

Kiedy zmieniasz na callback: () => Unitteraz, twoja sprawa przyjmuje po prostu funkcję, a nie parametr call-by-name. Oczywiście funkcja może być przechowywana w, val callbackwięc nie ma problemu.

Najłatwiejszym sposobem uzyskania tego, czego chcesz ( Scheduled(40, println("x") )gdzie parametr call-by-name jest używany do przekazania lambda) jest prawdopodobnie pominięcie casei jawne utworzenie tego apply, czego nie możesz uzyskać w pierwszej kolejności:

class Scheduled(val time: Int, val callback: () => Unit) {
    def doit = callback()
}

object Scheduled {
    def apply(time: Int, callback: => Unit) =
        new Scheduled(time, { () => callback })
}

W użyciu:

scala> Scheduled(1234, println("x"))
res0: Scheduled = Scheduled@5eb10190

scala> Scheduled(1234, println("x")).doit
x
Ben Jackson
źródło
3
Dlaczego nie zachować klasy wielkości liter i po prostu zastąpić domyślne zastosowanie? Ponadto kompilator nie może przetłumaczyć nazwiska na leniwego val, ponieważ mają one z natury inną semantykę, leniwy jest co najwyżej raz, a nazwa ma odniesienie do każdego odniesienia
Viktor Klang
@ViktorKlang Jak można zastąpić domyślną metodę stosowania klasy przypadku? stackoverflow.com/questions/2660975/…
Sawyer
obiekt ClassName {defin. zastosowanie (…):… =…}
Viktor Klang
Cztery lata później zdaję sobie sprawę, że wybrana przeze mnie odpowiedź odpowiadała tylko na pytanie w tytule, a nie to, które faktycznie miałem (na które ta odpowiedź odpowiada).
Malvolio
1

W pytaniu chcesz zasymulować funkcję SetTimeOut w JavaScript. Na podstawie wcześniejszych odpowiedzi piszę następujący kod:

class Scheduled(time: Int, cb: => Unit) {
  private def runCb = cb
}

object Scheduled {
  def apply(time: Int, cb: => Unit) = {
    val instance = new Scheduled(time, cb)
    Thread.sleep(time*1000)
    instance.runCb
  }
}

W REPL możemy uzyskać coś takiego:

scala> Scheduled(10, println("a")); Scheduled(1, println("b"))
a
b

Nasza symulacja nie zachowuje się dokładnie tak samo jak SetTimeOut, ponieważ nasza symulacja jest funkcją blokującą, ale SetTimeOut nie blokuje.

Jeff Xu
źródło
0

Robię to w ten sposób (po prostu nie chcę przerywać aplikacji):

case class Thing[A](..., lazy: () => A) {}
object Thing {
  def of[A](..., a: => A): Thing[A] = Thing(..., () => a)
}

i nazwij to

Thing.of(..., your_value)
Alexey Rykhalskiy
źródło