[Haskell-cafe] Help on syntactic sugar for combining lazy & strict monads?

Benjamin Redelings benjamin.redelings at gmail.com
Tue Jul 20 18:37:01 UTC 2021


Hi,

I'm working on a probabilistic programming language with Haskell syntax 
[1].  I am trying to figure out how to intermingle lazy and strict 
monadic code without requiring ugly-looking markers on the lazy code.  
Does anybody have insights on this?

1. I'm taking a monadic approach that is similar to [2], but I'm using a 
lazy interpreter.  This allows code such as the following, which would 
not terminate under a strict interpreter:

run_lazy $ do
   xs <- sequence $ repeat $ normal 0 1
   return $ take 10 xs

Here "xs" is an infinite list of Normal(0,1) random variables, of which 
only 10 are returned.  In a strict interpreter the line for "xs" never 
completes. But in a lazy interpreter, it works fine.

2. However, a lazy interpreter causes problems when trying to introduce 
*observation* statements (aka conditioning statements) into the monad 
[3].  For example,

run_lazy $ do
   x <- normal 0 1
   y <- normal x 1
   z <- normal y 1
   2.0 `observe_from` normal z 1
   return y

In the above code fragment, y will be forced because it is returned, and 
y will force x.  However, the "observe_from" statement will never be 
forced, because it does not produce a result that is demanded.

3. My current approach is to use TWO monads -- one for random sampling 
(Sample a), and one for observations (Observe a).  The random sampling 
monad can be lazy, because for random samples there is no need to force 
a sampling event if the result is never used.  The observation monad is 
strict, because all the observations must be forced.

So this WORKS fine.  However... the code looks ugly :-(  Help?

4a. One idea is to nest the lazy code within the strict monad, using 
some kind of tag "sample :: Sample a -> Observe a".  Then we could write 
something in the (Observe a) monad like:

run_strict $ do
   w <- sample $ sequence $ repeat $ normal 0 1
   x <- sample $ normal 0 1
   2.0 `observe_from` normal x 1
   y <- sample $ normal x 1
   z <- sample $ normal y 1
   2.0 `observe_from` normal z 1
   return y

When the "run_strict" interpreter encounters a statement of the form 
(sample $ _ ), it switches to the "run_lazy" interpreter for that statement.

In this case, the `observe_from` statement IS forced because it is in 
the strict (Observe a) monad.  Maybe somewhat surprisingly w, x, y, and 
z are forced (ugh!) -- by the outer strict interpreter, not the inner 
lazy interpreter. However, the internal components of w are NOT forced, 
so the program is able to terminate.

QUESTION: Is there some way of doing this without manually writing 
"sample" in front of all the sampling operations?

QUESTION: is there a way of doing this where the "sample $ _" lines do 
NOT have their result forced?

4b. In order to write "sample" less, it is possible to factor the 
sampling code into a separate function (here called "prior"):

prior :: Sample ([Double], Double, Double, Double)
prior = do
   w <- sequence $ repeat $ normal 0 1
   x <- normal 0 1
   y <- normal x 1
   z <- normal y 1
   return (w,x,y,z)

model :: Observe Double
model = do
   (w,x,y,z) <- sample $ prior
   2.0 `observe_from` normal x 1
   2.0 `observe_from` normal z 1
   return y

This does mean that you have to write "sample" only once... but it (i) 
splits the function in half and (ii) forces you to explicitly pass 
(w,x,y,z) between the two functions.  That obfuscates the code for no 
benefit.

Interestingly, the logical conclusion of this movement of code from 
(Observe a) to (Sample a) is to move EVERYTHING to the Sample monad:

prior :: Sample (Observe (), Double)
prior = do
   w <- sequence $ repeat $ normal 0 1
   x <- normal 0 1
   let observations1 = [2.0 `observe_from` normal x 1]
   y <- normal x 1
   z <- normal y 1
   let observations2 = [2.0 `observe_from` normal z 1]++observations1
   return (observations2,y)

model :: Observe Double
model = do
   (observations, y) <- sample $ prior
   sequence_ observations
   return y

Now, we have moved all the observations into the (Sample a) monad, but 
using horrible syntax!  Ugh :-(  Also, now the function "model" is 
basically the same for all models -- the entire model has been moved 
into the prior.

QUESTION:  is there some way to write a monadic function like "prior" 
while accumulating things observations in a list?

Thanks for any insights!

-BenRI

[1] http://bali-phy.org/models.php

[2] Practical probabilistic programming with monads, 
https://dl.acm.org/doi/10.1145/2804302.2804317

[3] https://github.com/tweag/monad-bayes/issues/32
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/haskell-cafe/attachments/20210720/6aa467cd/attachment.html>


More information about the Haskell-Cafe mailing list