Класс случая Scala запрещает параметры вызова по имени?
Я хочу реализовать бесконечный список:
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
проблема заключается в call-by-name
не допускается.
Я слышал, что это потому, что val
или же var
параметр конструктора не разрешен для call-by-name
, Например:
class A(val x: =>Int)
//error: `val' parameters may not be call-by-name
Но противоречие в том, что нормальный параметр конструктора все еще val
, несмотря на private
, Например:
class A(x: =>Int)
// pass
Итак, вопрос:
- Проблема действительно в
val
или жеvar
?- Если это. Так как смысл вызова по имени заключается в том, чтобы отложить вычисления, почему не мог
val
или жеvar
вычисление (или инициализация) будет отложено?
- Если это. Так как смысл вызова по имени заключается в том, чтобы отложить вычисления, почему не мог
- Как обойти класс cass для реализации бесконечного списка?
4 ответа
Здесь нет противоречия class A(x: => Int)
эквивалентно class A(private[this] val x: => Int)
и не class A(private val x: => Int)
, private[this]
помечает значение instance-private, тогда как модификатор private без дальнейшей спецификации позволяет получить доступ к значению из любого экземпляра этого класса.
К сожалению, определение case class A(private[this] val x: => Int)
тоже не разрешено. Я предполагаю, что это потому, что case-классам нужен доступ к значениям конструктора других экземпляров, потому что они реализуют equals
метод.
Тем не менее, вы могли бы реализовать функции, которые класс case предоставил бы вручную:
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)
}
Чтобы проверить этот код, вы можете попробовать:
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")
}
}
К сожалению, я не знаю наверняка, почему вызов по именам членов val и var запрещен. Тем не менее, есть по крайней мере одна опасность для него: подумайте о том, как классы реализации реализуют toString
; toString
-метод каждого значения конструктора называется. Это может (и в этом примере) привести к тому, что значения будут называться бесконечно. Вы можете проверить это, добавив t.toString
в MyNode
"s toString
-метод.
Изменить: После прочтения комментария Криса Мартина: Реализация equals
также создаст проблему, которая, вероятно, является более серьезной, чем реализация toString
(который в основном используется для отладки) и hashCode
(что приведет только к более высокой частоте столкновений, если вы не сможете учесть этот параметр). Вы должны тщательно продумать, как бы вы реализовали equals
быть значимым.
Я также не нашел, почему именно параметры по имени запрещены в случае классов. Я думаю, что объяснение должно быть довольно сложным и сложным. Но Рунар Бьярнасон в своей книге " Функциональное программирование в Scala" предлагает хороший подход для преодоления этого препятствия. Он использует понятие "гром" вместе с запоминанием. Вот пример реализации 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: _*))
}
}
Как видите, вместо обычного параметра по имени для конструктора данных класса дел они используют то, что они называют "thunk", функцией с нулевыми аргументами () => T
, Затем, чтобы сделать это прозрачным для пользователя, они объявляют умный конструктор в объекте-компаньоне, который позволяет вам предоставлять параметры по имени и делать их запоминающимися.
Это на самом деле похожий подход к Stream
решение, но упрощенное до того, что действительно требуется:
case class A(x: () => Int) {
lazy val xx = x()
}
Таким образом, вы можете использовать свой класс case как:
def heavyOperation: Int = ???
val myA = A(heavyOperation)
val myOtherA = A(() => 10)
val useA = myA.xx + myOtherA.xx
Таким образом, настоящая тяжелая операция будет выполняться только при использовании xx
только на последней строке.
Мне нравится использовать неявную функцию, чтобы преобразователь работал как вызов по имени.
например, в этом примере:
case class Timed[R](protected val block: () => R) {
override def toString() = s"Elapsed time: $elapsedTime"
val t0 = System.nanoTime()
val result = block() // execute thunk
val t1 = System.nanoTime()
val elapsedTime = t1 - t0
}
implicit def blockToThunk[R](bl: => R) = () => bl //helps to call Timed without the thunk syntax
это позволяет вам вызвать Timed({Thread.sleep(1000); println("hello")}), например, с синтаксисом вызова по имени