Functional Reactive Programming with Reflex and CodeWorld

TL;DR: I’m releasing a Reflex-based FRP interface to CodeWorld. It’s more complex, but also far more compositional than CodeWorld’s existing API.

Background

(You can skip this section if you already know a bit about CodeWorld.)

  • A plain pure function which, given a state and something that happens in that state, produces a new state.
  • A plain pure function which, given a state, produces a picture to be displayed on the screen.
A Gloss-model CodeWorld program

Intro to Reflex and FRP

(You can skip this section if you already know a bit about Reflex.)

  • A Behavior is a value that may change over time. This might be the current position of the mouse pointer, or the current score in a game.
  • A Dynamic is both a Behavior and an Event. It has a value over time, but the changes in that value are observable as an event that fires when updates are made.

CodeWorld’s Reflex Integration

You may have heard that getting started with Reflex is a pain. This is not true! What is true is that getting started with cross-platform development using reflex-dom and GHCJS can be painful. (There are projects like reflex-platform and Obelisk designed to mitigate that pain… your mileage may vary.) But Reflex itself is just a library, and can be installed the same way you’d install any other Haskell library.

import CodeWorld.Reflex
import Reflex
main = reflexOf $ \input -> return $
constDyn codeWorldLogo
reflexOf
:: (forall t m. (Reflex t, MonadHold t m, MonadFix m)
=> ReactiveInput t -> m (Dynamic t Picture))
-> IO ()
  • The output type is a Dynamic t Picture: a picture that can change over time. That should make sense! (If you’re curious why it’s a Dynamic instead of a Behavior, that’s because it would be a waste to keep redrawing a screen that’s not changing. Dynamic contains enough information to avoid redrawing when the screen doesn’t change.)
  • The input type is a ReactiveInput t. This is just a bundle of information you may want to use in your program. The Guide page tells you what you can get out of a ReactiveInput t.
keyPress        :: ReactiveInput t -> Event   t Text
keyRelease :: ReactiveInput t -> Event t Text
textEntry :: ReactiveInput t -> Event t Text
pointerPress :: ReactiveInput t -> Event t Point
pointerRelease :: ReactiveInput t -> Event t Point
pointerPosition :: ReactiveInput t -> Dynamic t Point
pointerDown :: ReactiveInput t -> Dynamic t Bool
currentTime :: ReactiveInput t -> Dynamic t Double
timePassing :: ReactiveInput t -> Event t Double
import CodeWorld.Reflex
import Reflex
main :: IO ()
main = reflexOf $ \input -> return $ do
let angle = vectorDirection <$> pointerPosition input
rotated <$> angle <*> pure needle
needle = solidRectangle 6 0.3

State

This is all well and good, but it doesn’t address the question of state. The compass relied on the position of the mouse pointer, so at least it’s more stateful than pure CodeWorld animations. But in a more complex program, you’ll want your own state. Everything we’ve seen so far requires that your program be a pure function of the dynamic values from the input bundle, and that’s very limiting!

{-# LANGUAGE OverloadedStrings #-}import CodeWorld.Reflex
import Control.Monad.Fix
import Reflex
main :: IO ()
main = reflexOf $ \input -> do
len <- needleLen input
let angle = vectorDirection <$> pointerPosition input
return $ rotated <$> angle <*> (needle <$> len)
needleLen
::
(Reflex t, MonadHold t m, MonadFix m)
=> ReactiveInput t -> m (Dynamic t Double)
needleLen input = do
let lenChange = fmapMaybe change (keyPress input)
foldDyn (+) 6 lenChange
where change "Up" = Just ( 1)
change "Down" = Just (-1)
change _ = Nothing
needle len = solidRectangle len 0.3
  • Here, though, one module of the program can safely create and manipulate state that is private to that module, and depends in well-defined ways on the remaining program state. That module can define how this state responds to various inputs, at various levels of abstraction. There is no action-at-a-distance, though, because the inputs to every piece of code are explicitly passed in. There’s no shared global state.

Traditional CodeWorld or Reflex?

I’m thrilled to offer this new choice for doing quick and powerful graphics programming in CodeWorld. However, I don’t expect it to replace uses of the simpler CodeWorld API.

  • QuickCheck tests, to show off property-based testing to others.
  • The traditional CodeWorld graphics API, for very concise and purely functional graphics demos, animations, and games.
  • And now Reflex-based FRP for bringing in more powerful abstractions and modularity.

Software engineer, volunteer K-12 math and computer science teacher, author of the CodeWorld platform, amateur ring theorist, and Haskell enthusiast.