Scala case class prohibits call-by-name parameters?
Asked Answered
D

4

27

Scenario:
I want to implement an infinite list:

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

Problem:
The error is that call-by-name is not allowed.

I've heard that it is because val or var constructor parameter is not allowed for call-by-name. For example:

class A(val x: =>Int) 
//error: `val' parameters may not be call-by-name

But in contrast the normal constructor parameter is still val, despite private. For example:

class A(x: =>Int) 
// pass

So the Question :

  • Is the problem really about val or var ?
    • If that, since the point for call-by-name is to defer computation. Why could not val or var computation(or initialization) be deferred?
  • How to get around the case class limitation to implement an infinite list?
Delmardelmer answered 5/11, 2014 at 4:10 Comment(1)
For an infinite data structure, what value does the case class sugar provide? equals, hashCode, toString won't work. And I'm not sure what I'd expect from unapply.Haugh
E
16

There is no contradiction: class A(x: => Int) is equivalent to class A(private[this] val x: => Int) and not class A(private val x: => Int). private[this] marks a value instance-private, while a private-modifier without further specification allows accessing the value from any instance of that class.

Unfortunately, defining a case class A(private[this] val x: => Int) is not allowed either. I assume it is because case-classes need access to the constructor values of other instances, because they implement the equals method.

Nevertheless, you could implement the features that a case class would provide manually:

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

To check this code, you could try:

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

Unfortunately, I do not know for sure why call-by-name val and var members are prohibited. However, there is at least one danger to it: Think about how case-classes implement toString; The toString-method of every constructor value is called. This could (and in this example would) lead to the values calling themselves infinitely. You can check this by adding t.toString to MyNode's toString-method.

Edit: After reading Chris Martin's comment: The implementation of equals will also pose a problem that is probably more severe than the implementation of toString (which is mostly used for debugging) and hashCode (which will only lead to higher collision rates if you can't take the parameter into account). You have to think carefully about how you would implement equals to be meaningfull.

Ekaterina answered 5/11, 2014 at 4:59 Comment(2)
I think you'd just leave equals the way it was, with the caveat that it will only work for terminating lists. That's how Stream works, right? Stream(1) == Stream(1) is true, but Stream.from(1) == Stream.from(1) doesn't halt.Haugh
@ChrisMartin Depends on how the list is used (without cycles, it is fine), but I'd probably implement a safer equals-method. One could for example pass a set of already visited MyList instances to a specialized equalsMyList(other: MyList[T], visited: Set[MyList[T]])-method. The specialized method could then check for recursion by checking whether this is already contained in visited, and return true in this case. The abortion criterion would probably need to check for object identity instead of regular equality, or we might run into the next endless recursion.Ekaterina
K
7

I have also not found why exactly by-name parameters are prohibited in case classes. I guess explanation should be quite elaborate and complex. But Runar Bjarnason in his book "Functional Programming in Scala" provides a good approach to handle this obstacle. He uses the concept of a "thunk" together with memoizing. Here is an example of Stream implementation:

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: _*))
 }
}

As you see, instead of a regular by-name parameter for the case class data constructor they use what they call a "thunk", a function of zero-arguments () => T. Then to make this transparent for the user they declare a smart constructor in the companion object which allows you to provide a by-name parameters and make them memoized.

Katinka answered 29/8, 2016 at 18:21 Comment(0)
B
1

This is actually similar approach to the Stream solution but simplified to what is actually required:

case class A(x: () => Int) {
  lazy val xx = x()
}

So you can use your case class as:

def heavyOperation: Int = ???
val myA = A(heavyOperation)
val myOtherA = A(() => 10)
val useA = myA.xx + myOtherA.xx

Like this the actual heavy operation will be performed only when you use xx, i.e., only on the last line.

Buffo answered 19/3, 2019 at 15:9 Comment(0)
P
0

I like using an implicit function to make a thunk work like a call by name.

e.g. in this example:

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

this let's you call Timed({Thread.sleep(1000); println("hello")}) for example with call by name syntax

Peregrinate answered 2/6, 2021 at 18:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.