Qual è la differenza tra =>, () => e Unit =>


153

Sto cercando di rappresentare una funzione che non accetta argomenti e non restituisce alcun valore (sto simulando la funzione setTimeout in JavaScript, se devi saperlo.)

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

non viene compilato, dicendo che "i parametri" val "potrebbero non essere chiamati per nome"

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

compila, ma deve essere invocato in modo strano, invece di

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

devo fare questo

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

Ciò che funziona è anche

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

ma è invocato in un modo anche meno sensibile

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

(Quale sarebbe una variabile di tipo Unit?) Quello che voglio ovviamente è un costruttore che possa essere invocato nel modo in cui lo invocherei se fosse una normale funzione:

 Scheduled(40, println("x") )

Dai al bambino il suo biberon!


3
Un altro modo di usare le classi case con parametri per nome è quello di metterle in un elenco di parametri secondario, ad es case class Scheduled(time: Int)(callback: => Unit). Questo funziona perché l'elenco dei parametri secondario non è esposto pubblicamente, né è incluso nei metodi equals/ generati hashCode.
nilskp,

Alcuni aspetti più interessanti riguardanti le differenze tra i parametri per nome e le funzioni 0-arity si trovano in questa domanda e nella risposta. In realtà è quello che stavo cercando quando ho trovato questa domanda.
lex82,

Risposte:


234

Chiamata per nome: => Tipo

La => Typenotazione sta per call-by-name, che è uno dei molti modi in cui i parametri possono essere passati. Se non hai familiarità con loro, ti consiglio di dedicare un po 'di tempo a leggere l'articolo di Wikipedia, anche se al giorno d'oggi è principalmente chiamata per valore e chiamata per riferimento.

Ciò che significa è che ciò che viene passato viene sostituito con il nome valore all'interno della funzione. Ad esempio, prendi questa funzione:

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

Se lo chiamo così

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

Quindi il codice verrà eseguito in questo modo

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

Tuttavia, ciò solleva il punto su ciò che accade se esiste uno scontro di nomi identificativi. Nella tradizionale chiamata per nome, un meccanismo chiamato sostituzione che evita la cattura ha luogo per evitare scontri tra nomi. In Scala, tuttavia, questo è implementato in un altro modo con lo stesso risultato: i nomi degli identificatori all'interno del parametro non possono fare riferimento o gli identificatori ombra nella funzione chiamata.

Ci sono altri punti relativi alla chiamata per nome di cui parlerò dopo aver spiegato gli altri due.

Funzioni 0-arity: () => Tipo

La sintassi () => Typesta per il tipo di a Function0. Cioè, una funzione che non accetta parametri e restituisce qualcosa. Ciò equivale, per esempio, a chiamare il metodo size(): non accetta parametri e restituisce un numero.

È interessante, tuttavia, che questa sintassi è molto simile alla sintassi per una funzione anonima letterale , che è la causa di un po 'di confusione. Per esempio,

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

è una funzione anonima letterale di arity 0, il cui tipo è

() => Unit

Quindi potremmo scrivere:

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

Tuttavia, è importante non confondere il tipo con il valore.

Unità => Tipo

Questo è in realtà solo un Function1, il cui primo parametro è di tipo Unit. Altri modi per scrivere sarebbe (Unit) => Typeo Function1[Unit, Type]. Il fatto è ... è improbabile che questo sia mai ciò che si vuole. Lo Unitscopo principale del tipo è quello di indicare un valore a cui non è interessato, quindi non ha senso ricevere quel valore.

Considera, ad esempio,

def f(x: Unit) = ...

Che cosa si potrebbe fare con x? Può avere un solo valore, quindi non è necessario riceverlo. Un possibile utilizzo sarebbe il concatenamento di funzioni che ritornano Unit:

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

Poiché andThenè definito solo Function1e le funzioni che stiamo concatenando stanno tornando Unit, abbiamo dovuto definirle come di tipo Function1[Unit, Unit]per poterle incatenare.

Fonti di confusione

La prima fonte di confusione sta pensando che la somiglianza tra tipo e letterale che esiste per le funzioni 0-arity esiste anche per call-by-name. In altre parole, pensando che, perché

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

è letterale per () => Unit, quindi

{ println("Hi!") }

sarebbe letterale per => Unit. Non è. Questo è un blocco di codice , non un letterale.

Un'altra fonte di confusione è Unitla scrittura del valore di quel tipo (), che sembra un elenco di parametri 0-arity (ma non lo è).


Potrei essere il primo a votare in meno dopo due anni. Qualcuno si sta chiedendo della sintassi case => a Natale e non posso raccomandare questa risposta come canonica e completa! A cosa sta arrivando il mondo? Forse i Maya erano appena partiti da una settimana. Hanno capito bene negli anni bisestili? Ora legale?
som-snytt il

@ som-snytt Bene, la domanda non è stata posta case ... =>, quindi non ho parlato. Triste ma vero. :-)
Daniel C. Sobral

1
@Daniel C. Sobral potresti per favore spiegare "Questo è un blocco di codice, non un letterale." parte. Quindi qual è la differenza esatta tra due?
nish1013,

2
@ nish1013 Un "letterale" è un valore (l'intero 1, il carattere 'a', la stringa "abc"o la funzione () => println("here"), per alcuni esempi). Può essere passato come argomento, archiviato in variabili, ecc. Un "blocco di codice" è una delimitazione sintattica di istruzioni - non è un valore, non può essere passato in giro o qualcosa del genere.
Daniel C. Sobral,

1
@Alex Questa è la stessa differenza di (Unit) => Typevs () => Type: il primo è a Function1[Unit, Type], mentre il secondo è a Function0[Type].
Daniel C. Sobral,

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

Il case modificatore rende implicito valda ogni argomento il costruttore. Quindi (come notato da qualcuno) se rimuovi casepuoi usare un parametro call-by-name. Il compilatore potrebbe probabilmente permetterlo comunque, ma potrebbe sorprendere le persone se creato val callbackinvece di trasformarsi in lazy val callback.

Quando passi a callback: () => Unitora il tuo caso accetta solo una funzione anziché un parametro di chiamata per nome. Ovviamente la funzione può essere memorizzata in val callbackmodo che non ci siano problemi.

Il modo più semplice per ottenere ciò che desideri ( Scheduled(40, println("x") )dove un parametro call-by-name viene utilizzato per passare un lambda) è probabilmente quello di saltare il casee creare esplicitamente applyciò che non potresti ottenere in primo luogo:

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

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

In uso:

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

scala> Scheduled(1234, println("x")).doit
x

3
Perché non tenerlo tra maiuscole e minuscole e sovrascrivere l'applicazione di default? Inoltre, il compilatore non può tradurre un nome in un valore pigro, poiché hanno una semantica intrinsecamente diversa, pigro è quasi una volta e il nome ha ogni riferimento
Viktor Klang,

@ViktorKlang Come si può ignorare il metodo di applicazione predefinito di una classe di casi? stackoverflow.com/questions/2660975/…
Sawyer,

object ClassName {def apply (...):… =…}
Viktor Klang

Quattro anni dopo mi rendo conto che la risposta che ho selezionato rispondeva solo alla domanda nel titolo, non a quella che avevo effettivamente (a cui questa risponde).
Malvolio,

1

Nella domanda, si desidera simulare la funzione SetTimeOut in JavaScript. Sulla base delle risposte precedenti, scrivo il seguente codice:

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, possiamo ottenere qualcosa del genere:

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

La nostra simulazione non si comporta esattamente come SetTimeOut, perché la nostra simulazione sta bloccando la funzione, ma SetTimeOut non è bloccante.


0

Lo faccio in questo modo (solo non voglio rompere applicare):

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

e chiamalo

Thing.of(..., your_value)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.