¿Cuál es la diferencia entre =>, () => y Unit =>

153

Estoy tratando de representar una función que no toma argumentos y no devuelve ningún valor (estoy simulando la función setTimeout en JavaScript, si debe saberlo).

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

no se compila, diciendo que "los parámetros 'val' pueden no ser llamados por nombre"

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

compila, pero tiene que ser invocado de forma extraña, en lugar de

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

Tengo que hacer esto

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

Lo que también funciona es

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

pero se invoca de una manera aún menos sensata

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

(¿Cuál sería una variable de tipo Unidad?) Lo que quiero por supuesto, es un constructor que se pueda invocar de la forma en que lo invocaría si fuera una función ordinaria:

 Scheduled(40, println("x") )

¡Dale al biberón su bebé!

Malvolio
fuente
3
Otra forma de usar clases de casos con parms por nombre es colocarlos en una lista de parámetros secundarios, por ejemplo case class Scheduled(time: Int)(callback: => Unit). Esto funciona porque la lista de parámetros secundarios no se expone públicamente, ni se incluye en los métodos equals/ generados hashCode.
nilskp
En esta pregunta y en la respuesta se encuentran algunos aspectos más interesantes sobre las diferencias entre los parámetros de nombre y las funciones de aria 0 . En realidad es lo que estaba buscando cuando encontré esta pregunta.
lex82

Respuestas:

234

Llamada por nombre: => Tipo

La => Typenotación significa llamada por nombre, que es una de las muchas formas en que se pueden pasar los parámetros. Si no está familiarizado con ellos, le recomiendo que se tome un tiempo para leer ese artículo de Wikipedia, aunque hoy en día es principalmente llamada por valor y llamada por referencia.

Lo que significa es que lo que se pasa se sustituye por el nombre del valor dentro de la función. Por ejemplo, tome esta función:

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

Si lo llamo así

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

Entonces el código se ejecutará así

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

Aunque eso plantea el punto de lo que sucede si hay un choque de nombre de identificador. En la llamada tradicional por nombre, se lleva a cabo un mecanismo llamado sustitución para evitar la captura para evitar conflictos de nombres. En Scala, sin embargo, esto se implementa de otra manera con el mismo resultado: los nombres de los identificadores dentro del parámetro no pueden referirse a los identificadores de sombra en la función llamada.

Hay algunos otros puntos relacionados con la llamada por nombre de los que hablaré después de explicar los otros dos.

Funciones 0-arity: () => Tipo

La sintaxis () => Typerepresenta el tipo de a Function0. Es decir, una función que no toma parámetros y devuelve algo. Esto es equivalente a, por ejemplo, llamar al métodosize() : no toma parámetros y devuelve un número.

Sin embargo, es interesante que esta sintaxis es muy similar a la sintaxis de una función literal anónima , lo que es motivo de cierta confusión. Por ejemplo,

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

es una función anónima literal de arity 0, cuyo tipo es

() => Unit

Entonces podríamos escribir:

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

Sin embargo, es importante no confundir el tipo con el valor.

Unidad => Tipo

Esto es en realidad solo un Function1, cuyo primer parámetro es de tipo Unit. Otras formas de escribirlo serían (Unit) => Typeo Function1[Unit, Type]. La cosa es ... es poco probable que esto sea lo que uno quiere. El Unitpropósito principal del tipo es indicar un valor que no le interesa, por lo que no tiene sentido recibir ese valor.

Considere, por ejemplo,

def f(x: Unit) = ...

¿Con qué se podría hacer x? Solo puede tener un valor único, por lo que no es necesario recibirlo. Un posible uso sería encadenar funciones que devuelven Unit:

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

Debido a andThenque solo se define en Function1y las funciones que estamos encadenando están regresando Unit, tuvimos que definirlas como de tipo Function1[Unit, Unit]para poder encadenarlas.

Fuentes de confusión

La primera fuente de confusión es pensar que la similitud entre el tipo y el literal que existe para las funciones de aridad 0 también existe para la llamada por nombre. En otras palabras, pensar eso, porque

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

es un literal para () => Unit, entonces

{ println("Hi!") }

sería un literal para => Unit. No lo es. Ese es un bloque de codigo , no un literal.

Otra fuente de confusión es que Unitel valor de ese tipo está escrito (), lo que parece una lista de parámetros de 0 aridades (pero no lo es).

Daniel C. Sobral
fuente
Puede que tenga que ser el primero en votar después de dos años. Alguien se pregunta sobre la sintaxis case => en Navidad, ¡y no puedo recomendar esta respuesta como canónica y completa! ¿A qué viene el mundo? Tal vez los mayas acababan de irse por una semana. ¿Figuraron en los años bisiestos correctamente? ¿Horario de verano?
som-snytt
@ som-snytt Bueno, la pregunta no se hizo case ... =>, así que no lo mencioné. Triste pero cierto. :-)
Daniel C. Sobral
1
@Daniel C. Sobral, ¿podría explicar por favor "Ese es un bloque de código, no un literal". parte. Entonces, ¿cuál es la diferencia exacta entre dos?
nish1013
2
@ nish1013 Un "literal" es un valor (el entero 1, el carácter 'a', la cadena "abc"o la función () => println("here"), para algunos ejemplos). Se puede pasar como argumento, almacenarse en variables, etc. Un "bloque de código" es una delimitación sintáctica de declaraciones: no es un valor, no se puede pasar ni nada por el estilo.
Daniel C. Sobral
1
@Alex Esa es la misma diferencia que (Unit) => Typevs () => Type: la primera es a Function1[Unit, Type], mientras que la segunda es a Function0[Type].
Daniel C. Sobral
36
case class Scheduled(time : Int, callback :  => Unit)

El casemodificador hace implícito valde cada argumento al constructor. Por lo tanto (como alguien señaló) si elimina casepuede usar un parámetro de llamada por nombre. El compilador probablemente podría permitirlo de todos modos, pero podría sorprender a las personas si se creara en val callbacklugar de transformarse lazy val callback.

Cuando cambia a callback: () => Unitahora, su caso solo toma una función en lugar de un parámetro de llamada por nombre. Obviamente, la función se puede almacenar val callbackpara que no haya ningún problema.

La forma más fácil de obtener lo que desea ( Scheduled(40, println("x") )donde se usa un parámetro de llamada por nombre para pasar una lambda) es probablemente omitir casey crear explícitamente lo applyque no pudo obtener en primer lugar:

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

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

En uso:

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

scala> Scheduled(1234, println("x")).doit
x
Ben Jackson
fuente
3
¿Por qué no mantener una clase de caso y simplemente anular la aplicación predeterminada? Además, el compilador no puede traducir un by-name a un vago val, ya que tienen una semántica inherentemente diferente, lazy es a lo sumo una vez y su nombre tiene cada referencia
Viktor Klang,
@ViktorKlang ¿Cómo puede anular el método de aplicación predeterminado de una clase de caso? stackoverflow.com/questions/2660975/…
Sawyer
object ClassName {def apply (...): ... = ...}
Viktor Klang
Cuatro años después y me doy cuenta de que la respuesta que seleccioné solo respondió la pregunta en el título, no la que realmente tenía (que esta sí contesta).
Malvolio
1

En la pregunta, desea simular la función SetTimeOut en JavaScript. Basado en respuestas anteriores, escribo el siguiente código:

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

En REPL, podemos obtener algo como esto:

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

Nuestra simulación no se comporta exactamente igual que SetTimeOut, porque nuestra simulación es función de bloqueo, pero SetTimeOut no es de bloqueo.

Jeff Xu
fuente
0

Lo hago de esta manera (simplemente no quiero romper aplicar):

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

y llámalo

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