Dikstra's 1968 Goto Considered Harmful argues that we can produce better code by replacing goto with structured commands, such as functions, if-statements, and loops. In this tradition, the functional programming community has continued to demonstrate how we can produce better programs without various side-effects, sometimes with the introduction of new language features. For example, many forms of mutable state can be expressed with recursion.
Of course these effects aren't truly eliminated but merely shifted down the technological stack. The compiler for a language with recursion will likely utilize mutable state for an efficient implementation. The benefits of pure languages are also their costs: being cut off from operational details let us focus on essential complexity, but at the same time inhibit us improving the performance of our programs. Hopefully these costs will continue to decrease over time due to faster and bigger chips, and advanced compilation techniques. Modern compilers for high level languages already produce competitive speed and space efficiency for contexts where resources aren't extremely tight.
Modern functional programming languages like Haskell demonstrate that pure functional programming without side-effects is practical and indeed desirable in many contexts. For the remaining contexts, Haskell programmers use the IO Monad as an escape hatch for arbitrary effects, such as reading from and writing to files, the console, databases, and network connections. While technically pure, the IO Monad essentially creates a separate silo for effects in otherwise pure code. It is the ongoing quest of the pure functional programming to demonstrate how we can do more and more of our work without resorting to effects at all.
Functional Reactive Programming (FRP) was a big step forward for this effort. FRP demonstrated how reactive systems such as robotics and user interfaces can be programmed without effects. Instead of manually redrawing graphics or engaging motors, FRP programs declaratively describe dynamic relationships between time-varying values. The effectful hardware commands are delegated to the FRP library.
Data persistence and distributed computing are two areas the pure FP community has yet to "abstract over." We still use the IO Monad to generate the effects of reading from and writing to the filesystem or database, as well as sending and receiving data across the network. Most programmers either never question these interfaces or consider them inevitable. FP zealots hold out hope that we'll one day embrace them into the fold.
What appears as two problems is in fact one. Data persistence is essentially sending to a place with particular guarantees about how you'll be able to retrieve it later. A local datastore behaves much like a remote one in that it has non-negligible latency, albeit much less than a remote one.
We can be sure what the pure versions of these interfaces won't be. We won't be manually setting various pieces of data for persistence, mutating them, and cleaning them up. We already know how to program with automatic RAM memory management, so we can be confident that a compiler can handle the same task with persistent memory "on disk".
The same goes doesn't network requests. We can be sure that we won't be manually making and receiving network requests one by one. That's analogous to manually querying and mutating the HTML DOM with JQuery. FRP showed us how to declaratively describe our view (HTML) as a function of a time-varying state, and leave it to the FRP engine, such as ReactJS, to make the appropriate DOM mutations for us as necessary.
Let's start by upgrading the simplest FRP example, a button counter application, to a distributed button counter. First a local counter:
instance Functor Event ...
instance Foldable Event ...
count :: Event a -> Behavior Int
count event = foldl (+) 0 (map (const 1) event)
instance Monad (Dom a) ...
button :: Text -> Dom (Event ())
span :: Text -> Dom ()
counter :: Dom ()
counter = button "Click Me" >>= \clicks -> span $ count clicks
runDom :: DomSelector -> Dom -> Monad IO
runDom "#app" counterThe dynamic counter application is expressed entirely purely. For example, counter >>= counter would produce two equivalent but independent counters side-by-side. But like all pure computations, we do need one effect: evaluate me. In non-graphical Haskell programs, the runtime engine is usually implicit -- evaluate this code on this computer's processor, with this computer's RAM -- but we can specify how much RAM on the command line. In this counter, we need to be a bit more explicit about where we want this application to live on the screen. The imperative runDom function runs the counter application at the DOM node with id app.
Similarly, for our distributed counter we need to specify a "runtime engine" that will carry out the computation for us. We can assume the engine is a publicly accessible server that we identify with a URL.
newtype DistributedEvent a = DistributedEvent (Event a) Computer Time
distributed :: Event a -> DistributedEvent a
lift :: (Event a -> b) -> DistributedEvent a -> b
counterDistributed :: Dom ()
counterDistributed = button "Click Me" >>= \clicks -> span $ lift count $ distributed clicks
runDom :: DomSelector -> Dom -> Monad IO
runDomDistributed "#app" "https://distributedfrp.com/jsSksl34KelwN" counterDistributed- why is effect-free good?
- why is IO monad not truly effect free?
- denotational design separates performance from essential complexity --> why is this the right separation?
- do I need to explain FRP behaviors and events better?
- distributed behavior
- persistance that lives or doesn't live past page refresh (timing of when to accumulate ... FRP Now?)
- encryption and permission (read and write)