Subject: Re: [bug#76485] [PATCH] gexp: ‘with-parameters’ properly handles‘%graft?’.

From: David Elsing <[email-masked]>
Date: Sat, 01 Mar 2025 17:52:31 +0000

Hi Ludo',

I thought about it some more and I think my issue is that in general,
the number of times a function passed to bind is evaluated is not fixed.
Attached is an example for a monad which acts as a combination of the
state monad and the maybe monad. I used two parameters, because they are
set at different times in the implementation of `mparameterize' (but
this seems to be a different issue to me). In particular, p2 is not
restored at the end, because no function application is done for a
"nothing" value.

Another problematic example would be a "coroutine" monad [1], which can
pause the evaluation of its "steps", during which the parameters could
be used or changed elsewhere.

Ludovic Courtès <[email-masked]> writes:

> Yes, though it would be nice to have a variant of ‘parameterize’ “that
> works for any monad”, that was the intent of ‘mparameterize’.

I think what you describe (although I'm not too familiar with Haskell)
is like the ReaderT (or StateT if the parameters can change within the
monad) monad transformer, where the parameters are stored in the
additional state provided by ReaderT. This is however a new monad and
different from setting the parameters globally in different functions
passed to bind.

> Because of that, I have a preference for ‘mparameterize’ rather than
> ‘state-parameterize’ or any other specific variant.
>
> WDYT?

To my understanding, what we actually want is to affect the way the
function of the state monad applies to the passed state, or formulated
in a different way, the state to include the parameters. This would be
effectively achieved using `with-fluids*' inside the monadic procedure
(except that they are not part of the final state). It can be though of
expanding the state of the state monad with the parameters, where the
initial state contains the "outside" parameters. Of course, now there
are two different ways to pass on state, through the state monad and the
parameters... :)

Does this make sense?

Because `with-fluids*' does not work with prompts, I still think your
solution is a good workaround when specialized for the state monad, as
long as the parameters are not used externally until the execution is
completely finished.

> Yes, that’s because ‘with-fluids*’ is implemented in C, which makes it a
> “continuation barrier” (continuations that contain C stack frames cannot
> be resumed).  It’s a limitation of the current implementation rather
> than a bug, strictly speaking.  :-)

Ah I see, thanks for the explanation! Are there plans to change that and
do you think it would be difficult to do?

Maybe the suspension could also be done without prompts by instead
modifying the store monad, similar to [1]? It would probably be less
straightforward though and maybe better suitable for languages like
Haskell.

Cheers,
David

[1] https://hackage.haskell.org/package/monad-coroutine-0.9.2/docs/Control-Monad-Coroutine.html

(use-modules
 (guix monads)
 (ice-9 match))

(define-inlinable (test-return value)
  (lambda (state)
    (list 'just value state)))

(define-inlinable (test-bind mvalue mproc)
  (lambda (state)
    (match (mvalue state)
      (('just value state2)
       ((mproc value) state2))
      (('nothing state2)
       (list 'nothing state2)))))

(define-monad %test-monad
  (bind test-bind)
  (return test-return))

(define mval
  (mlet %test-monad
      ((a (return 3))
       (b (lambda (state) (list 'nothing state))))
    (return 2)))

(define p1 (make-parameter 1))
(define p2 (make-parameter 1))

(pk "A" (p1) (p2))

(define mval2
  (mparameterize %test-monad
      ((p1 3)
       (p2 3))
    mval))

(pk "B" (p1) (p2))

(p1 2)
(p2 2)

(pk "C" (p1) (p2))

(pk (mval2 1))

(pk "D" (p1) (p2))

;;; Output:

;;; ("A" 1 1)

;;; ("B" 3 1)

;;; ("C" 2 2)

;;; ((nothing 1))

;;; ("D" 2 3)