telefonica - ¿La clase de caso Scala prohíbe los parámetros de llamada por nombre?
identificador de llamadas telefonica (2)
Quiero implementar una lista infinita:
abstract class MyList[+T]
case object MyNil extends MyList[Nothing]
case class MyNode[T](h:T,t: => MyList[T]) extends MyList[T]
//error: `val'' parameters may not be call-by-name
El problema es que la call-by-name
no está permitida.
He oído que se debe a que el parámetro constructor de val
o var
no está permitido para la call-by-name
. Por ejemplo:
class A(val x: =>Int)
//error: `val'' parameters may not be call-by-name
Pero la contradicción es que el parámetro constructor normal sigue siendo val
, a pesar de private
. Por ejemplo:
class A(x: =>Int)
// pass
Así que la pregunta:
- ¿Es realmente el problema de
val
ovar
?- Si eso. Dado que el punto de llamada por nombre es aplazar el cálculo, ¿por qué no se podría aplazar el cálculo o la inicialización de
val
ovar
?
- Si eso. Dado que el punto de llamada por nombre es aplazar el cálculo, ¿por qué no se podría aplazar el cálculo o la inicialización de
- ¿Cómo sortear la clase cass para implementar una lista infinita?
No hay contradicción: la class A(x: => Int)
es equivalente a la class A(private[this] val x: => Int)
y no la class A(private val x: => Int)
. private[this]
marca un valor instancia-privado, mientras que un modificador privado sin más especificación permite acceder al valor desde cualquier instancia de esa clase.
Desafortunadamente, la definición de una case class A(private[this] val x: => Int)
tampoco está permitida. Supongo que es porque las clases de casos necesitan acceso a los valores de constructor de otras instancias, porque implementan el método equals
.
Sin embargo, podría implementar las características que una clase de caso proporcionaría manualmente:
abstract class MyList[+T]
class MyNode[T](val h: T, t: => MyList[T]) extends MyList[T]{
def getT = t // we need to be able to access t
/* EDIT: Actually, this will also lead to an infinite recursion
override def equals(other: Any): Boolean = other match{
case MyNode(i, y) if (getT == y) && (h == i) => true
case _ => false
}*/
override def hashCode = h.hashCode
override def toString = "MyNode[" + h + "]"
}
object MyNode {
def apply[T](h: T, t: => MyList[T]) = new MyNode(h, t)
def unapply[T](n: MyNode[T]) = Some(n.h -> n.getT)
}
Para verificar este código, puedes probar:
def main(args: Array[String]): Unit = {
lazy val first: MyNode[String] = MyNode("hello", second)
lazy val second: MyNode[String] = MyNode("world", first)
println(first)
println(second)
first match {
case MyNode("hello", s) => println("the second node is " + s)
case _ => println("false")
}
}
Lamentablemente, no sé con seguridad por qué están prohibidos los miembros val y var de call by name. Sin embargo, hay al menos un peligro: piense en cómo las clases de casos implementan toString
; Se toString
método toString
de cada valor de constructor. Esto podría (y en este ejemplo) conduciría a que los valores se llamen a sí mismos infinitamente. Puede verificar esto agregando t.toString
a MyNode
''s toString
-method.
Edit: Después de leer el comentario de Chris Martin: La implementación de equals
también planteará un problema que probablemente sea más grave que la implementación de toString
(que se usa principalmente para la depuración) y hashCode
(que solo conducirá a mayores tasas de colisión si es posible) t tener en cuenta el parametro). Tienes que pensar cuidadosamente acerca de cómo implementar equals
para ser significativo.
Tampoco he encontrado por qué están prohibidos exactamente los parámetros de by-name en las clases de casos. Supongo que la explicación debe ser bastante elaborada y compleja. Pero Runar Bjarnason en su libro " Programación funcional en Scala " proporciona un buen enfoque para manejar este obstáculo. Él usa el concepto de "thunk" junto con la memorización. Aquí hay un ejemplo de implementación de Stream:
sealed trait Stream[+A]
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]
object Stream {
def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {
lazy val head = hd
lazy val tail = tl
Cons(() => head, () => tail)
}
def empty[A]: Stream[A] = Empty
def apply[A](as: A*): Stream[A] =
if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))
}
}
Como puede ver, en lugar de un parámetro por nombre regular para el constructor de datos de clase de caso, utilizan lo que llaman "thunk", una función de cero argumentos () => T
Luego, para hacer esto transparente para el usuario, declaran un constructor inteligente en el objeto complementario que le permite proporcionar parámetros por nombre y hacerlos memorizados.