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 Effect
s.
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 Effect
ful 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 Effect
s.
2 Likes