Functional Reactive Programming with Reflex and CodeWorld

Background

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

  • An initial state
  • 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.)

  • An Event is a thing that can happen. This might be something at a low level of abstraction, like a mouse being clicked. Or it might be something at a much higher level, such as moving a chess piece. But it happens at specific times, yielding specific values that describe what occurred. (When I first encountered FRP several years ago, the word “Event” was very confusing to me. An FRP Event is not a single event. It’s more like a stream of events that may continue occurring, yielding different values. By now, though, the word has unfortunately caught on.)
  • 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 argument to the reflexOf function is a function of its own. That makes sense: you passed a lambda for that argument in the starter program. That function is a rank 2 type with a forall and context, but ignore that context line, and focus on its base type:
    ReactiveInput t -> m (Dynamic t Picture).
  • 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
  • In traditional CodeWorld programs, composition was possible for each event type. Within an event handler, one could delegate, decompose, and abstract changes. But different event handlers communicated via a shared global state! In fact, it wasn’t common to see a student solve a problem by storing a value like 0.001 into a variable, planning for some completely different event handler to react some way if the value is greater than 0. These kinds of spooky action-at-a-distance dependencies are exactly what functional programming tries to avoid by eschewing mutable state!
  • 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.

  • Console-mode programs, replacing some uses of GHCi with a platform that gives you easily shareable code snippets to send to others who don’t have Haskell installed.
  • 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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Chris Smith

Chris Smith

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