Was ist der Unterschied zwischen =>, () => und Unit =>

153

Ich versuche, eine Funktion darzustellen, die keine Argumente akzeptiert und keinen Wert zurückgibt (ich simuliere die Funktion setTimeout in JavaScript, wenn Sie es wissen müssen.)

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

wird nicht kompiliert und sagt, dass "val" -Parameter möglicherweise nicht nach Namen aufgerufen werden "

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

kompiliert, muss aber seltsamerweise aufgerufen werden, anstatt

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

Ich muss das tun

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

Was auch funktioniert ist

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

wird aber noch weniger vernünftig aufgerufen

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

(Was wäre eine Variable vom Typ Unit?) Was ich natürlich möchte, ist ein Konstruktor, der so aufgerufen werden kann, wie ich ihn aufrufen würde, wenn es eine gewöhnliche Funktion wäre:

 Scheduled(40, println("x") )

Gib Baby seine Flasche!

Malvolio
quelle
3
Eine andere Möglichkeit, Fallklassen mit By-Name-Parametern zu verwenden, besteht darin, sie in eine sekundäre Parameterliste aufzunehmen, z case class Scheduled(time: Int)(callback: => Unit). Dies funktioniert, weil die sekundäre Parameterliste weder öffentlich verfügbar gemacht noch in den generierten equals/ hashCodeMethoden enthalten ist.
Nilskp
In dieser Frage und der Antwort finden sich einige weitere interessante Aspekte hinsichtlich der Unterschiede zwischen By-Name-Parametern und 0-Arity-Funktionen . Es ist eigentlich das, wonach ich gesucht habe, als ich diese Frage gefunden habe.
Lex82

Antworten:

234

Call-by-Name: => Typ

Die => TypeNotation steht für Call-by-Name. Dies ist eine der vielen Möglichkeiten, wie Parameter übergeben werden können. Wenn Sie mit ihnen nicht vertraut sind, empfehle ich Ihnen, sich etwas Zeit zu nehmen, um diesen Wikipedia-Artikel zu lesen, obwohl es heutzutage meistens Call-by-Value und Call-by-Reference ist.

Was es bedeutet, dass das, was passiert ist , ersetzt für die Wertnamen innerhalb der Funktion. Nehmen Sie zum Beispiel diese Funktion:

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

Wenn ich es so nenne

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

Dann wird der Code so ausgeführt

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

Dies wirft jedoch den Punkt auf, was passiert, wenn ein Identifikationsnamenskonflikt auftritt. Bei herkömmlichen Call-by-Name-Vorgängen findet ein Mechanismus statt, der als Substitution zur Vermeidung von Captures bezeichnet wird, um Namenskonflikte zu vermeiden. In Scala wird dies jedoch auf andere Weise mit demselben Ergebnis implementiert - Bezeichnernamen innerhalb des Parameters können in der aufgerufenen Funktion nicht auf Bezeichner verweisen oder diese beschatten.

Es gibt einige andere Punkte im Zusammenhang mit Call-by-Name, von denen ich sprechen werde, nachdem ich die beiden anderen erklärt habe.

0-arity-Funktionen: () => Typ

Die Syntax () => Typesteht für den Typ a Function0. Das heißt, eine Funktion, die keine Parameter akzeptiert und etwas zurückgibt. Dies entspricht beispielsweise dem Aufruf der Methode size()- es werden keine Parameter verwendet und eine Zahl zurückgegeben.

Es ist jedoch interessant, dass diese Syntax der Syntax für ein anonymes Funktionsliteral sehr ähnlich ist , was zu Verwirrung führt. Beispielsweise,

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

ist ein anonymes Funktionsliteral der Arität 0, dessen Typ ist

() => Unit

Also könnten wir schreiben:

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

Es ist jedoch wichtig, den Typ nicht mit dem Wert zu verwechseln.

Einheit => Typ

Dies ist eigentlich nur ein Function1, dessen erster Parameter vom Typ ist Unit. Andere Möglichkeiten, es zu schreiben, wären (Unit) => Typeoder Function1[Unit, Type]. Die Sache ist ... es ist unwahrscheinlich, dass dies jemals das ist, was man will. Der UnitHauptzweck des Typs besteht darin, einen Wert anzugeben, an dem man nicht interessiert ist. Daher ist es nicht sinnvoll, diesen Wert zu erhalten .

Betrachten Sie zum Beispiel

def f(x: Unit) = ...

Was könnte man möglicherweise damit machen x? Es kann nur einen einzigen Wert haben, daher muss man ihn nicht erhalten. Eine mögliche Verwendung wäre die Rückgabe von Verkettungsfunktionen Unit:

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

Da andThennur für definiert Function1ist und die Funktionen, die wir verketten, zurückkehren Unit, mussten wir sie als vom Typ definieren, um sie verketten Function1[Unit, Unit]zu können.

Quellen der Verwirrung

Die erste Quelle der Verwirrung ist der Gedanke, dass die Ähnlichkeit zwischen Typ und Literal, die für 0-Arity-Funktionen besteht, auch für Call-by-Name besteht. Mit anderen Worten, das zu denken, weil

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

für eine wörtliche () => Unit, dann

{ println("Hi!") }

wäre ein wörtliches für => Unit. Es ist nicht. Das ist ein Codeblock , kein Literal.

Eine weitere Quelle der Verwirrung ist, dass Unitder Wert des Typs geschrieben wird (), der wie eine Parameterliste mit 0 Aritäten aussieht (aber nicht).

Daniel C. Sobral
quelle
Möglicherweise muss ich nach zwei Jahren der erste sein, der eine Abstimmung durchführt. Jemand wundert sich über case => Syntax an Weihnachten, und ich kann diese Antwort nicht als kanonisch und vollständig empfehlen! Zu was kommt die Welt? Vielleicht waren die Mayas nur eine Woche weg. Haben sie in Schaltjahren richtig gerechnet? Sommerzeit?
Som-Snytt
@ som-snytt Nun, die Frage wurde nicht gestellt case ... =>, also habe ich sie nicht erwähnt. Traurig aber wahr. :-)
Daniel C. Sobral
1
@ Daniel C. Sobral Könnten Sie bitte erklären "Das ist ein Codeblock, kein Literal." Teil. Was ist der genaue Unterschied zwischen zwei?
Nish1013
2
@ nish1013 Ein "Literal" ist ein Wert ( für einige Beispiele die Ganzzahl 1, das Zeichen 'a', die Zeichenfolge "abc"oder die Funktion () => println("here")). Es kann als Argument übergeben, in Variablen usw. gespeichert werden. Ein "Codeblock" ist eine syntaktische Abgrenzung von Anweisungen - es ist kein Wert, es kann nicht weitergegeben werden oder so etwas.
Daniel C. Sobral
1
@Alex Das ist der gleiche Unterschied wie (Unit) => Typevs () => Type- der erste ist a Function1[Unit, Type], während der zweite a ist Function0[Type].
Daniel C. Sobral
36
case class Scheduled(time : Int, callback :  => Unit)

Der caseModifikator impliziert valaus jedem Argument den Konstruktor. Daher können Sie (wie bereits erwähnt) beim Entfernen caseeinen Call-by-Name-Parameter verwenden. Der Compiler könnte es wahrscheinlich trotzdem zulassen, aber es könnte die Leute überraschen, wenn es erstellt wird, val callbackanstatt sich in etwas zu verwandeln lazy val callback.

Wenn Sie zu callback: () => Unitjetzt wechseln, übernimmt Ihr Fall nur eine Funktion und keinen Call-by-Name-Parameter. Natürlich kann die Funktion in gespeichert werden, val callbackso dass es kein Problem gibt.

Der einfachste Weg, um das zu bekommen, was Sie wollen ( Scheduled(40, println("x") )wobei ein Call-by-Name-Parameter verwendet wird, um ein Lambda zu übergeben), besteht wahrscheinlich darin, das zu überspringen caseund explizit das zu erstellen apply, was Sie überhaupt nicht bekommen konnten:

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

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

In Benutzung:

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

scala> Scheduled(1234, println("x")).doit
x
Ben Jackson
quelle
3
Warum nicht eine Fallklasse beibehalten und einfach die Standardanwendung überschreiben? Außerdem kann der Compiler einen Nachnamen nicht in einen Lazy Val übersetzen, da sie von Natur aus eine andere Semantik haben, Lazy höchstens einmal ist und By-Name bei jeder Referenz einen Namen hat
Viktor Klang
@ViktorKlang Wie können Sie die Standardanwendungsmethode einer Fallklasse überschreiben? stackoverflow.com/questions/2660975/…
Sawyer
Objekt ClassName {def apply (…):… =…}
Viktor Klang
Vier Jahre später stelle ich fest, dass die Antwort, die ich ausgewählt habe, nur die Frage im Titel beantwortet hat, nicht die, die ich tatsächlich hatte (die diese beantwortet).
Malvolio
1

In der Frage möchten Sie die SetTimeOut-Funktion in JavaScript simulieren. Basierend auf früheren Antworten schreibe ich folgenden Code:

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
  }
}

In REPL können wir so etwas bekommen:

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

Unsere Simulation verhält sich nicht genau wie SetTimeOut, da unsere Simulation die Funktion blockiert, SetTimeOut jedoch nicht blockiert.

Jeff Xu
quelle
0

Ich mache es so (will nur nicht brechen bewerben):

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

und nenne es

Thing.of(..., your_value)
Alexey Rykhalskiy
quelle