Functional Reactive Programming with Reflex and CodeWorld

Chris Smith
Jul 1 · 10 min read

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.)

Since its inception, my CodeWorld project has always subscribed to the gloss programming model. That is, in essence, that a program consists of:

  • 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.

This is, I think, the simplest general-purpose programming model that is possible. (CodeWorld has a few simpler models, but they require programs to be stateless, so do not really qualify as “general-purpose”.) It has been reinvented many times, and I’ve heard it described by various communities with names like “functional MVC”, or “the universe model”. It is also closely related to dynamical systems from mathematical modeling (which do not pictures or UI events) or the Elm Architecture (which extends it with a system called “ports” for outgoing I/O).

This model is radically simple, but it’s not very compositional. It’s difficult to abstract over parts of the application, or break apart a complex program into smaller pieces. I have written several medium-sized applications in this model, and Joachim Breitner even programmed his slides for a talk within CodeWorld in this model (and wrote a glorious hack to be able to build them in CodeWorld itself). I think we’d all agree we’ve stretched the upper scaling limits of this simple model.

There is also an alternative model with a long history now in functional programming: Functional Reactive Programming (FRP).

Intro to Reflex and FRP

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

Functional reactive programming has been around for a long time, now, with a dizzying list of implementations. But over the last several years, the Reflex library (https://reflex-frp.org/) has proven to be a stable, reliable, and production-ready implementation of the idea, and it appears to be rising to dominance. In Reflex, FRP is implemented in terms of three basic abstractions:

  • 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.

It’s often said that in FRP, an event is like a list of timestamped values (so similar to [(Time, a)], and a behavior is like a function from time to a value (so Time -> a). This is true in the sense of general mathematical values — but it’s not true if you think of these as Haskell types. Sure, a behavior has a value for each point of time, but that function isn’t computable, because it depends on things the user has yet to do. It only makes sense to interact with that value in a limited sense: you can ask for the current value, but you can’t sensibly expect to be able to ask for the value 100 years from now, or 300 years ago!

So to ensure that you only interact with these values in well-defined coherent ways, they are abstract data types, with their own APIs. One of the nice things for Haskell programmers is that these APIs are not all new in FRP: Event is a Functor, and Behavior and Dynamic are Applicative and Monad instances, as well. That gives these types a familiar API. All of these abstractions are also Monoids and Semigroups. But on top of this, there are more specialized combinators for combining them, at https://github.com/reflex-frp/reflex/blob/develop/Quickref.md

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.

Now it’s even easier, though: All you need is a web browser!

To use Reflex with CodeWorld, just hop over to http://code.world/haskell, and type something like this:

import CodeWorld.Reflex
import Reflex
main = reflexOf $ \input -> return $
constDyn codeWorldLogo

Click the Run button, and you’ll see a beautiful CodeWorld logo drawn via Reflex!

Hmm… not impressed? Okay, I admit that functional reactive programming is more interesting if you it can actually, well, react to things. To do this, you’ll want to look at the definition of reflexOf. Hit the “Guide” button at the bottom of your screen, and choose the Reflex section on the topic bar to the left.

reflexOf
:: (forall t m. (Reflex t, MonadHold t m, MonadFix m)
=> ReactiveInput t -> m (Dynamic t Picture))
-> IO ()

Let’s take this in parts.

  • 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

If you’re familiar with the basic CodeWorld API, some of these should look familiar. With Reflex, though, CodeWorld doesn’t use its own event type at all. You get your events straight from the FRP abstractions in this input bundle, which are sometimes much nicer to use. For example, you no longer need to stash away the most recent mouse pointer position in a field somewhere, because it’s always available from the pointerPosition dynamic value.

Let’s do something with these values. Here’s a compass that always points n̶o̶r̶t̶h̶ toward the mouse pointer.

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

This is using only Functor and Applicative operations. First, we decide which direction to rotate the needle, by calculating the direction toward the current mouse pointer position. The Functor class lets us apply this calculation to a dynamic value, and get a dynamic result called angle. Next, we apply the rotated function using Applicative syntax to build a dynamic picture. (Applicative wasn’t actually required here, but it’s available and looks cleaner than writing ($ needle) . rotated <$> angle to avoid it.)

Something to pay attention to is how we’re still doing what’s known as “wholemeal programming”. This is the opposite of piecemeal. Instead of focusing on what’s happening on what’s happening at some particular moment, we just say, in essence, that the picture is a needle, rotated in the direction of the current mouse position. It would be a mistake to think of this as an action that’s run at some specific time: it’s instead a specification for how the picture is related to the mouse position in this program, and that’s enough!

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!

To manipulate state, you’ll use the monad that’s called m in the type signature for reflexOf. It’s also sometimes referred to as the “builder monad”. Instead of writing reflexOf $ \input -> return $ ... (which ignores the builder monad), we can write code inside of it. Let’s make the length of our compass needle adjustable.

{-# 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

Because the needle length is new state, it must be defined in the builder monad. To do that, we first use fmapMaybe on the built-in keyPress event, to extract just presses from the relevant keys and what change they should add to the cumulative length. We then use foldDyn to build a stateful Dynamic that remembers the state over time.

Notice that we’re now postponing the return in the builder monad until the last line of main. That lets us use the builder monad to construct this new dynamic value that holds onto state.

It’s worth comparing the way stateful programs typically break down in traditional CodeWorld (and Elm, and Racket universe, and similar models) versus the Reflex case.

  • 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.

The cost, though, is the need to use all these new abstractions. Composition sometimes comes at a cost in complexity.

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.

I don’t ever intend to introduce Reflex or any kind of FRP in the CodeWorld education dialect. It’s just not suited for teaching new programmers. Even for new Haskell programmers, such as in university classes, I’d strongly recommend avoiding Reflex for a first exposure to the language. That said, though, I’m really excited about Reflex providing even more of a smooth path from early learning experiences into more abstract functional programming. Abstraction may be the enemy of the beginner, but it is not the enemy of the intermediate programmer! Many of us love Haskell for providing powerful tools to solve complex problems. Being able to transition from the traditional CodeWorld model to Reflex without relearning the output format or UI concepts opens up some really cool possibilities.

Another direction I’m very much pursuing is to use this new interface to clean up the implementation of CodeWorld itself! CodeWorld started out with a hand-coded tail-recursive event loop, and as it’s accumulated new features, time has not been kind to the implementation. I’m already making plans to reimplement many of CodeWorld’s debugging features on top of Reflex, so that eventually this new implementation will be the implementation of CodeWorld, and the simpler variants will be implemented on top of this base. This change shouldn’t be visible to users, but anyone who has recently browsed the implementation may appreciate the change!

I’m also interested in what the community wants to see happen. I am excited about the possibility of using CodeWorld to demo and share Haskell with others. CodeWorld can already be used for:

  • 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.

I’d also love to incorporate a traditional gloss implementation for compatibility with other people’s gloss demos, and well as a diagrams backend. If there are other libraries or techniques that you’d like to be able to show off easily using a web-based platform, open a bug or ask, please.

Thanks for listening!

Chris Smith

Written by

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