Класс случая 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")}), например, с синтаксисом вызова по имени

Другие вопросы по тегам