Whether it's Observable, Google Colab or Jupyter, I love interactive notebooks. If none of these names are familiar, such notebooks are documents where regular text snippets, headings etc. mingle with working code snippets. They are great communication tools and allow a kind of fluid play with code that is hard to get inside regular IDE's.
I have been interested in making my own interactive notebooks for a long time not because there was anything missing in existing solutions, but because a lot of the times I find them to be too flexible: if a notebook allows its user to change everything, they can also be break its logic pretty easily and with little help towards recovery.
And besides, how much effort would it take to make an interactive notebook on my own terms anyway...
Well, without good crutches, probably a lot, so I didn't attempt it right until I stumbled upon a package in the Elm programming language called elm-markup
. Turns out, making a basic interactive notebook in it is much simpler than I had imagined: just under 200 lines and without even touching the package's more involved modules.
This experiment is what brings me to writing this series of blog posts, covering a delightful journey looking at interactive notebooks using elm-markup
. We will start with a simple counter app, working our way up to some interactive drawings.
What is elm-markup
?
It is officially described as the Elm-friendly markup language, one that brings Elm's promise of type safety and friendly error messages into the world of markup languages. Roughly speaking, I would sum it up as a kind of markup language that exposes its own internal structure (AST) so we can render it as we please: plain text, HTML, elm-ui
, or any other Elm value really. Spoiler: in this post, we will be turning markup into functions.
tldr the full example with comments is available in this Ellie. The rest of the post will go over it in detail.
A basic elm-markup
document
Let's define a simple Title
block. This block would tell elm-markup
that whenever it sees this piece of text:
|> Title
Interactive counteradder
It should render the indented text inside an h1
tag. The code for that would look like this:
titleBlock : Mark.Block (Html msg)
titleBlock =
Mark.block "Title"
(\str ->
h1 [] [ text str ]
)
Mark.string
Using this block, we can create a document like so:
Mark.compile
(Mark.document identity titleBlock)
"""
|> Title
Interactive counteradder
"""
Rendering this into a running Elm app involves a few more steps that will become clear later in the post. If you're curious, the package docs are your friends.
Adding a counter
The second block we will define is a named counter that renders some UI and emits the updated value as a message. The markup syntax looks like this:
|> Counter
name = var1
And without further ado, here is the code for it:
counterBlock : Mark.Block (Html ( String, Float ))
counterBlock =
Mark.record "Counter"
(\name ->
let
-- This is a placeholder value
-- We'll wire this up to proper state values in the next step
val = 0
in
div
[]
[ text (name ++ " = ")
, button
[ onClick ( name, val - 1 )
]
[ text "-"
]
, text (String.fromFloat val)
, button
[ onClick ( name, val + 1 )
]
[ text "+"
]
]
)
|> Mark.field "name" Mark.string
|> Mark.toBlock
Adding interactivity
So far, this elm-markup
document looks pretty static. In order to make it interactive and link counters to actual values by name, we will parse these them into functions that will allow us to inject values stored in the application model. This mind-bend is probably best illustrated by this shift in type signatures for the rendered block:
-- before
counterBlock : Mark.Block (Html ( String, Float ))
counterBlock = _
-- after
type alias Values = Dict.Dict String Float
counterBlock : Mark.Block (Values -> Html ( String, Float ))
counterBlock = _
There is nothing inherent in elm-markup
that would stop us from taking this shift: if we're in charge of what value a piece of markup renders to, it might as well be a function. We can use this function to inject a dictionary of Values
directly from our model and wire up the (String, Float)
events that changes it in response to interacting with the counter buttons.
Our finished counter block looks like this:
-- We will use this helper throughout the rest of the example
getValue : String -> Values -> Float
getValue name values =
values
|> Dict.get name
-- Values that are not kept track of yet are assumed to be the default
|> Maybe.withDefault 0
counterBlock : Mark.Block (Values -> Html ( String, Float ))
counterBlock =
Mark.record "Counter"
(\name values ->
let
value = getValue name values
in
div
[]
[ text (name ++ " = ")
, button
[ onClick ( name, value - 1 )
]
[ text "-"
]
, text (String.fromFloat value)
, button
[ onClick ( name, value + 1 )
]
[ text "+"
]
]
)
|> Mark.field "name" Mark.string
|> Mark.toBlock
Doing something with our values
Now that our wiring is complete, we can simply define a Sum
block that works with this markup:
|> Counter
name = var1
|> Counter
name = var2
|> Sum
arg1 = var1
arg2 = var2
What this block will do is simply take the values from the two named counters, add them together, and communicate the result as var1 + var2 == 3
.
The sum block implementation looks like this:
sumBlock : Mark.Block (Values -> Html ( String, Float ))
sumBlock =
Mark.record "Sum"
(\arg1 arg2 values ->
let
res =
getValue arg1 values + getValue arg2 values
in
div
[]
[ text (arg1 ++ " + " ++ arg2 ++ " == ")
, text (String.fromFloat res)
]
)
|> Mark.field "arg1" Mark.string
|> Mark.field "arg2" Mark.string
|> Mark.toBlock
Wiring it all up
Now that we have our blocks, we can write a method that compiles a document like this one:
markup : String
markup =
"""
|> Title
Interactive counteradder
|> Counter
name = var1
|> Counter
name = var2
|> Sum
arg1 = var1
arg2 = var2
"""
The compiler for it would look like this:
compileMarkup : String -> Result String (Values -> Html ( String, Float ))
compileMarkup markdownBody =
Mark.compile
(Mark.document
identity
(Mark.manyOf
[ titleBlock
, counterBlock
, sumBlock
]
)
)
markdownBody
|> (\res ->
case res of
Mark.Success blocks ->
Ok
(\data ->
div []
(List.map
-- Inject data into each block
-- This makes them into regular `elm-html` nodes
(\block -> block data)
blocks
)
)
_ ->
Err "Compile error"
)
And finally, the main model
, update
and view
:
type alias Model =
{ values : Values
}
type Msg
= SetValue ( String, Float )
update : Msg -> Model -> Model
update msg model =
case msg of
SetValue ( key, val ) ->
{ model
| values =
Dict.insert key
val
model.values
}
view : Model -> Html Msg
view model =
case compileMarkup markup of
-- The success case yields a function that takes the current `Values` dictionary
Ok viewByData ->
viewByData model.values
-- Map to the program message
|> map SetValue
Err err ->
text err
For a full implementation, head to the full example Ellie or the same code as a gist.
Where will we go next?
I know, I know, adding numbers is not that exciting. Eventually, we'll add sliders to make elm-webgl
drawings like this one, interactive.
Until then ☺️
Top comments (0)