简体   繁体   中英

Scala lazy evaluation and apply function

I'm following a book's example to implement a Steam class using lazy evaluation in Scala.

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

Then I used a simple function to test if it's working

def printAndReturn: Int = {
    println("called")
    1
}

Then I construct Stream like the following:

    println(s"apply: ${
        Stream(
            printAndReturn,
            printAndReturn,
            printAndReturn,
            printAndReturn
        )
    }")

The output is like this:

called
called
called
called
apply: Cons(fpinscala.datastructures.Stream$$$Lambda$7/1170794006@e580929,fpinscala.datastructures.Stream$$$Lambda$8/1289479439@4c203ea1)

Then I constructed Stream using cons :

println(s"cons: ${
    cons(
        printAndReturn,
        cons(
            printAndReturn,
            cons(printAndReturn, Empty)
        )
    )
}")

The output is:

cons: Cons(fpinscala.datastructures.Stream$$$Lambda$7/1170794006@2133c8f8,fpinscala.datastructures.Stream$$$Lambda$8/1289479439@43a25848)

So here are two questions:

  1. When constructing Stream using the apply function, all printAndReturn are evaluated. Is this because the recursive call to apply(as.head, ...) evaluates every head?
  2. If the answer to the first question is true, then how do I change apply to make it not force evaluation?
  1. No. If you put a breakpoint on the println you'll find that the method is actually being called when you first create the Stream . The line Stream(printAndReturn, ... actually calls your method however many times you put it there. Why? Consider the type signatures for cons and apply :

     def cons[A](hd: => A, tl: => Stream[A]): Stream[A]

    vs:

     def apply[A](as: A*): Stream[A]

    Note that the definition for cons has its parameters marked as => A . This is a by-name parameter. Declaring an input like this makes it lazy, delaying its evaluation until it is actually used. Hence your println will never get called using cons . Compare this to apply . You're not using a by name parameter and therefore anything that gets passed in to that method will automatically get evaluated.

  2. Unfortunately there isn't a super easy way as of now. What you really want is something like def apply[A](as: (=>A)*): Stream[A] but unfortunately Scala does not support vararg by name parameters. See this answer for a few ideas on how to get around this. One way is to just wrap your function calls when creating the Stream:

     Stream( () => printAndReturn, () => printAndReturn, () => printAndReturn, () => printAndReturn)

    Which will then delay the evaluation.

When you called

Stream(
            printAndReturn,
            printAndReturn,
            printAndReturn,
            printAndReturn
        )

the apply in the companion object was invoked. Looking at the parameter type of the apply, you would notice that it is strict. So the arguments will be evaluated first before being assigned to as . What as becomes is an Array of Ints

For 2, you can define apply as

def apply[A](as: (() => A)*): Stream[A] =
    if (as.isEmpty) empty else cons(as.head(), apply(as.tail: _*))

and as was suggested above, you need to pass the arguments as thunks themselves as in

println(s"apply: ${Stream(
    () => printAndReturn,
    () => printAndReturn,
    () => printAndReturn,
    () => printAndReturn
  )}")

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM