Skip to content

Instantly share code, notes, and snippets.

@zudov
Last active May 6, 2024 05:15
Show Gist options
  • Save zudov/65447685838ea8b2569f to your computer and use it in GitHub Desktop.
Save zudov/65447685838ea8b2569f to your computer and use it in GitHub Desktop.
Cycle's approach to handling the IO

Cycle.js approach to handling IO looks similar to how it was done in earlier FRP implementations. That seems to be an independent discovery, and that's always even better.

Yampa provides a function reactimate:

:: IO a	-- ^ IO initialization action
-> (Bool -> IO (DTime, Maybe a)) -- ^ IO input sensing action
-> (Bool -> b -> IO Bool)        -- ^ IO actuaction (output processing) action
-> SF a b	-- ^ Signal function
-> IO ()

First parameter is IO a, initialization action is an additional "init" function which Cycle doesn't have/need.

Second two parameters are important, they could be combined into a cycle driver:

Starting with input sensing action removing unrelated parts it's IO (Maybe a) which is a side-effecty function that 'Maybe' provides some output. In Cycle this would be a 'source' part of driver which is an input of main. Something that would return you the events of user interaction.

The output actuation action is essentially of a type b -> IO () what it does is that it takes the output and does something with it possibly producing side-effects. It can render it to the screen, play with virtual-dom, whatever. In Cycle this is handled by a 'sink' part of driver which is output of main.

The last parameter SF a b (note that a and b correspond to the same things as in previous type signatures). That is a "Signal Function" which semantically is Signal a -> Signal b. In case of Cycle, Observable is used in place of Signal. Cycle's main is basically the same signal/observable function which has a type Observable a -> Observable b (Cycle's version also provides nicer management of multiple observables, which could be implemented on top of that). And note, that a comes from the input sensing function, and b goes to output processing function.

So alltogether, I would say that Cycle's run could be implemented on top of reactimate (with some hacks probably).

The idea is that run provides the same essense of interaction with user, but wraps it in a nicer API @staltz. Note how cluttered are input/output actions API, Cycle.js cycles them together in a driver and leaves most of the lifting to RxJS.

The greatest thing is that main/SF a b part, is exactly identical and that's the most important part.

I also noted how similar the terminology is, particularly "senses" and "actuations/responses". That's a great independent discovery.

A more practical example

I've recently made a simple/incomplete reimplementation of Yampa in Purescript, and it features an example of simple game. Yampy-cube play here: https://github.com/zudov/purescript-miniyampa/blob/master/examples/yampy-cube/src/Main.purs

If you look at the source, it's very pure and functional except for the one part:

runDrawingApp sf ctx inputRef inputMod = do
  rh <- reactInit (readRef inputRef) (\_ _ -> render) sf
  lastInteractionRef <- do Milliseconds t0 <- nowEpochMilliseconds
                           newRef t0
  let step = do
        Milliseconds now <- nowEpochMilliseconds
        lastInteraction <- readRef lastInteractionRef
        let dt = now - lastInteraction
        writeRef lastInteractionRef now
        input <- readRef inputRef
        modifyRef inputRef inputMod
        react rh $ Tuple (dt / 1000.0) $ Just input
        requestAnimationFrame step
  requestAnimationFrame step

main = do
  Just canvas <- C.getCanvasElementById "canvas"
  ctx <- C.getContext2D canvas
  canvasJQ <- JQ.select "canvas"
  inputRef <- newRef { increment: NoEvent }
  JQ.on "click" (\_ _ -> modifyRef inputRef (_ { increment = Event 1 })) canvasJQ
  runDrawingApp (game >>> updated >>^ map render) ctx inputRef (_ { increment = NoEvent })

That's the only imperative part which actually just sets up inputs/outputs and feeds them to the processing loop. I have a feeling that Cycle could clean up that clutter greatly. I should try to bind them together and update that example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment