Behaviour of let statement in an Effect

How does let work in the following context?

I have an ffi with what is essentially an event handler for key presses as below, the details of the ffi are not really important. Now, with the let syntax as below, ticked only gets assigned once, rather than recomputing every time the code block below executes. So ev never changes, no matter how many key presses arrive.

    keyPressed event do
      let ticked = tick event
      res <- EVar.tryPut s{ ev = ticked } aVar

By comparison, changing the last line to the below and doing away with the let, means ev is assigned with the updated event every call.

      res <- EVar.tryPut s{ ev = tick event } aVar

Have I stumbled upon some screwy ffi code - or is this how let is supposed to work?

Thanks for any thoughts.

It should roughly translate to creating an intermediate var ticked = tick event or not creating it. So I’m not sure what difference can it make. Can you show some bigger chunk of code, i.e. how do you use the keyPressed function, and how do the two approaches differ it their behaviour in specific situations?

1 Like

It’s a bug that those two examples don’t behave identically; see Evaluation of Effect with magic-do optimization is inconsistent with other monadic code · Issue #3913 · purescript/purescript · GitHub.

However, it’s the first example, the one with the let, that is working as we would want it to. The compiler expects the evaluation of tick event to be pure, meaning that it doesn’t matter (except for performance) if it’s evaluated once or many times, and so evaluating it once should be sufficient. tick should be written such that any side effects are delayed until the function returned from tick event (which is what an Effect is, under the hood) is itself invoked.

1 Like

I was looking at the ffi code for the library I am using which is purescript-p5. The handler which is a regular js callback, is passed in directly as an effect. Here is the type signature.

keyPressed :: P5 -> (Effect Boolean) -> (Effect Unit)

So event in my code sample in my first post is of type P5, and the second argument to keyPressed is the do block you see there. I had a look at some ffis I’ve written for some libraries in the past, take this one example of a user state callback from Aws Cognito:

foreign import onAuthUIStateChangeImpl :: EffectFn1 (EffectFn2 String (Nullable User) Unit) (EffectFnCb Unit)

The key difference I can see between the two code samples above, is the latter models the callback as a EffectFn2, the former as an Effect.

I guess I shouldn’t beat around the bush, is it correct to model a js callback as simply an Effect in this fashion? I suppose if one has no use for the arguments passed to the callback it is. The use of let in such a do block causes me some mental gymnastics, as in my head I picture every line in that do block executing every time the callback fires, or equivalently, the do block is called.

Here’s the original source for keyPressed

The issue seems to be with your tick function rather than with the keyPressed function. If tick is indeed pure (not some sort of Effect), then it wouldn’t make any difference when tick was executed or how many times. That’s in fact the core idea behind “referential transparency,” which is a cornerstone concept in pure functional languages like PureScript. Referential transparency means that (performance aside) the body of a binding can be substituted for its name without changing the meaning. Here, if you had

let ticked = tick event
keyPressed event do
  res <- EVar.tryPut s{ ev = ticked } aVar

you should be able to substitute the body of ticked for the name:

keyPressed event do
  res <- EVar.tryPut s{ ev = tick event } aVar

without changing the meaning. The fact that these two aren’t the same thing means that tick isn’t pure - that there’s an Effect of some sort wrapped up in there.

Since PureScript believes tick event to be pure, it figures that it can just move that outside of the do block as an optimization. If you change the signature of tick to represent the Effect that it is running, and then change your keyPressed to

keyPressed event do
  ticked <- tick  event
  res <- EVar.tryPut s{ ev = ticked } aVar

then you should get the behavior that you want.

3 Likes

Right, so ticked just reads a property off the event, so currently it is indeed not in Effect. However this event you see in the code above, lives in a higher scope, and does change between invocations of keyPressed.

In essence the behaviour of the original js lib is to get a callback, and in that callback, query some global state object for what has changed. And this pattern is being mimicked in the ffi.

If event is mutable, then reading a property off of it is indeed effectful. For example, look at the Ref module, which is the typical way of working with mutable variables in PureScript. In that module, creating a Ref, writing to a Ref, and reading a Ref are all Effects.
In this case “effect” might mean something slightly different than you would intuitively think. Here, “effect” just means that the output depends on more than just the inputs. You could pass the same event into the tick function and get different output at different times, so the output depends on when it was executed in addition to the input.
Another great example is now, which just grabs the current time, but is considered an Effect, since it depends on context beyond it’s “input” (in this case, there is no “input”, and the value depends solely on additional context).

2 Likes

Thank you, you and @rhendric have just crystallised this for me.

1 Like

Personally, really like the description of Effectful vs “pure” functions from Jordan’s Reference.
Using the terms from that link, tick isn’t pure because of non-determinism, while keyPressed isn’t pure because of side-effects, but both things result in them being Effects.

2 Likes