¿Cuál es la diferencia entre =>, ()=> y Unidad=>?
Estoy tratando de representar una función que no acepta argumentos y no devuelve ningún valor (estoy simulando la función setTimeout en JavaScript, si es necesario saberlo).
case class Scheduled(time : Int, callback : => Unit)
no se compila y dice "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") } )
(¿Qué sería una variable de tipo Unidad?) Lo que quiero, por supuesto, es un constructor que pueda invocarse de la misma manera que lo haría si fuera una función ordinaria:
Scheduled(40, println("x") )
¡Dale al bebé su biberón!
Llamada por nombre: => Tipo
La => Type
notación significa llamada por nombre, que es una de las muchas formas en que se pueden pasar 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 se trata principalmente de 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 la cuestión de lo que sucede si hay un conflicto de nombres de identificadores. En la llamada por nombre tradicional, se lleva a cabo un mecanismo llamado sustitución que evita 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 hacer referencia a los identificadores ocultos 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 de aridad 0: () => Tipo
La sintaxis () => Type
representa el tipo de Function0
. Es decir, una función que no toma parámetros y devuelve algo. Esto equivale, por ejemplo, a llamar al método size()
: no toma parámetros y devuelve un número.
Es interesante, sin embargo, que esta sintaxis sea muy similar a la sintaxis de una función literal anónima , lo que genera cierta confusión. Por ejemplo,
() => println("I'm an anonymous function")
es una función anónima literal de aridad 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
En realidad, esto es solo un Function1
cuyo primer parámetro es de tipo Unit
. Otras formas de escribirlo serían (Unit) => Type
o Function1[Unit, Type]
. La cuestión es... es poco probable que esto sea alguna vez lo que uno quiere. El Unit
propósito principal del tipo es indicar un valor que no le interesa, por lo que no tiene sentido recibir ese valor.
Consideremos, por ejemplo,
def f(x: Unit) = ...
¿ Qué se podría hacer con x
? Solo puede tener un valor único, por lo que no es necesario recibirlo. Un posible uso sería encadenar funciones que devuelvan Unit
:
val f = (x: Unit) => println("I'm f")
val g = (x: Unit) => println("I'm g")
val h = f andThen g
Como andThen
solo está definido en Function1
, y 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 tipo y literal que existe para funciones de aridad 0 también existe para llamada por nombre. En otras palabras, pensar que, porque
() => { println("Hi!") }
es un literal para () => Unit
, entonces
{ println("Hi!") }
sería un literal para => Unit
. No lo es. Eso es un bloque de código , no un literal.
Otra fuente de confusión es que se escribe el valorUnit
del tipo , que parece una lista de parámetros de aridad 0 (pero no lo es).()
case class Scheduled(time : Int, callback : => Unit)
El case
modificador hace implícito val
cada argumento para el constructor. Por lo tanto (como alguien señaló), si elimina, case
puede usar un parámetro de llamada por nombre. El compilador probablemente podría permitirlo de todos modos, pero podría sorprender a la gente si creara val callback
en lugar de transformarse en lazy val callback
.
Cuando cambia a callback: () => Unit
ahora, su caso simplemente toma una función en lugar de un parámetro de llamada por nombre. Obviamente la función se puede almacenar val callback
así que no hay 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 case
y crear explícitamente lo apply
que 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
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 una función de bloqueo, pero SetTimeOut no es de bloqueo.