An endless list makes the REPL work for a long time and this is intentional (updated title)

In Haskell, because of lazy evaluation, this is a legitimate expression:

image

Daml doesn’t have lazy evaluation, as I was told recently. The same expression sends the REPL into an infinite loop:

image

Is that intentional? (My guess is it isn’t.)

BTW, why is it that Daml doesn’t have lazy evaluation?

2 Likes

It is.

I think the motives have changed over time, but nowadays I would say that proving properties of programs is easier if you do not have to cope with “actually nonterminating but kind of not if you really think about it” constructs like your sample. Additionally, all contract data must be strict, so the usefulness of lazy evaluation would be limited to ephemeral data structures, if it was supported at all.

3 Likes

I understand the second part, but not why it is desirable to send the REPL into an infinite loop instead of throwing an error.

1 Like

This is less of a deliberate decision and more a consequence of the fact that we make no attempts at checking for termination. In general, this is of course undecidable (halting problem and all that). You could detect some specific cases like [1..] but so far it hasn’t seemed worth the effort and arguably might lead to a false sense of security where users expect that we always detect this.

3 Likes

Ok, understood, thank you.

1 Like

In addition to what @Stephen already mentioned, I think there are two reasons that still apply:

  1. A lot more programmers are used to strict evaluation. The barrier of entry for learning Daml is already relatively high so matching the evaluation model people are used to has some advantages in that regard. It does cause confusion for people coming from Haskell but at least at this stage, that’s not how most people come towards Daml.
  2. Implementing efficient runtimes/interpreters for lazy languages is usually significantly more complex and much less well explored than doing the same for strict languages. Outside of the Haskell runtime, there are relatively few examples that do this well. Interpreters for strict languages on the other hand exist in countless numbers and even implementing a reasonably efficient one is still a relatively well-explored field.
2 Likes

Yes, sounds reasonable, thank you

1 Like

Just one more question, going back to my starting point.

The signature of the take function is like this:

take

: Int → [a] → [a]

Take the first n elements of a list.

This means, the REPL typechecks the [1..] expression as a list when interpreting the take 3 [1..] expression, is that correct given the fact that without lazy evaluation the [1..] part doesn’t make any sense?

1 Like

Typechecking is a static property independent of execution. [1..] is perfectly well typed as [Int].

2 Likes

Ranges like [1..], [1,3,..], [1..5], [1,3..11] are just syntactic sugar for enumFrom, enumFromThen, enumFromTo, and enumFromThenTo, respectively.

So take 3 [1..] is the same as take 3 (enumFrom 1). enumFrom : a -> [a] so [1..] does indeed have type [Int].

If you look at how this is implemented, you see that [1..] == enumFrom 1 == enumFromTo 1 maxBound == eftInt 1 maxBound where

eftInt : Int -> Int -> [Int]
-- [x1..x2]
eftInt x y | x > y = []
           | x == y = [x]
           | otherwise = x :: eftInt (x + 1) y
    maxBound =  0x7FFFFFFFFFFFFFFF

In other words, your expressions will terminate. It’ll just take a while. You recurse roughly 10^19 times.

2 Likes

Oh yes, thank you. In this case the correct description is not that the REPL gets into an infinite loop, but that it will work for a long time.

Now I understand it.

1 Like

Did it pretty quickly on my machine (in that the heap blew up before it terminated). More interestingly it:

  1. Ran
  2. Appeared to return to console without any output (but started ignoring any input)
  3. Obvious a process was still running because my laptop fan was going wild
  4. Then the heap blew up
  5. Then gave me a terminal where I couldn’t actually do anything anymore
$ daml repl
daml> take 3 [1..]
daml> Uncaught error from thread [Repl-akka.actor.default-dispatcher-11]: Java heap space, shutting down JVM since 'akka.jvm-exit-on-fatal-error' is enabled for ActorSystem[Repl]
java.lang.OutOfMemoryError: Java heap space
	at java.base/java.util.Arrays.copyOf(Arrays.java:3690)
	at java.base/java.util.ArrayList.grow(ArrayList.java:237)
	at java.base/java.util.ArrayList.grow(ArrayList.java:242)
	at java.base/java.util.ArrayList.add(ArrayList.java:485)
	at java.base/java.util.ArrayList.add(ArrayList.java:498)
	at com.daml.lf.speedy.Speedy$KPushTo.execute(Speedy.scala:1130)
	at com.daml.lf.speedy.Speedy$Machine.run(Speedy.scala:391)
	at com.daml.lf.engine.script.Runner.stepToValue$1(Runner.scala:465)
	at com.daml.lf.engine.script.Runner.$anonfun$runWithClients$64(Runner.scala:829)
	at com.daml.lf.engine.script.Runner$$Lambda$903/0x000000080179f040.apply(Unknown Source)
	at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:307)
	at scala.concurrent.Future$$Lambda$273/0x0000000801409840.apply(Unknown Source)
	at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:41)
	at scala.concurrent.impl.Promise$$Lambda$274/0x000000080140a840.apply(Unknown Source)
	at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64)
	at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:56)
	at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:93)
	at akka.dispatch.BatchingExecutor$BlockableBatch$$Lambda$271/0x00000008013f0c40.apply$mcV$sp(Unknown Source)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85)
	at akka.dispatch.BatchingExecutor$BlockableBatch.run(BatchingExecutor.scala:93)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:48)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:48)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)

daml> take 3 [1..]
daml> take 3 [1,2,3,4]
daml> 

I’ll file a bug report. I think the REPL should either fail completely or give me back a usable terminal but not this. It’s deceptive in the output but the line daml> Uncaught error... actually was daml> before the heap blew up and started printing to the terminal.

Edit: Opened an issue

1 Like