I heard several times people feeling uneasy when exposed to the Elm syntax for the first time.
Familiarity plays an important role when looking at a new language and Elm is probably more familiar to Haskell developers than to Javascript developers.
In the tutorial ๐ฃ Kaiten Sushi ๐ฃ Approaches to Web Animations I wrote the same animation both in Elm and in Javascript.
Here I will compare the code side by side. I know that is a bit like comparing apples and oranges but, why not?
The code has been adjusted for this comparison so it is not either the best Javascript nor the best Elm.
I also didnโt replicate The Elm Architecture in Javascript because... it was too much.
But enough talking. Let's get to the code
The View
-- Elm
view model =
[ img [ id "kaiten", src "svg/background.svg", onClick ClickOnPage ] []
, div [ id "homeLink" ]
[ a [ href "https://lucamug.github.io/kaiten-sushi/" ]
[ img [ src "svg/home.svg" ] [] ]
]
, div [ id "title" ] [ text "04 - VANILLA ELM - CLICK ANYWHERE"]
, div ([ id "sushi" ] ++ changeStyle model.currentState) [ text "๐ฃ" ]
]
<!-- HTML -->
<img id="kaiten" src="svg/background.svg" onclick="clickOnPage()">
<div id="homeLink">
<a href="https://lucamug.github.io/kaiten-sushi/">
<img src="svg/home.svg">
</a>
</div>
<div id="title">03 - VANILLA JAVASCRIPT - CLICK ANYWHERE</div>
<div id="sushi">๐ฃ</div>
The Javascript version uses plain HTML. Elm has a
view
function that generates the DOM at runtime through a Virtual DOM. It is the analogues of JSX in React but in plain Elm codeThe Elm view needs the text to be the argument of the
text
function. We cannot just put it there similar to HTML or JSXIn Elm, for each HTML element there is a correspondent function that gets two lists as arguments. The first list are the attributes, the second are the children elements
Because it is just Elm language, we can call functions and use data directly (see the
title
orchangeStyle
for example). Actually in Elm more thanchangeStyle
is rathergenerateStyle
On click Elm sends out the message ClickOnPage while Javascript calls directly the clickOnPage function. Think of messages as kind of events
The changeStyle function
-- Elm
changeStyle { scale, x } =
[ style "transform" ("scale(" ++ String.fromFloat scale ++ ")")
, style "left" (String.fromFloat x ++ "px")
]
// Javascript
function changeStyle(scale, x) {
sushi.style.transform = "scale(" + scale + ")";
sushi.style.left = x + "px";
}
++
vs.+
to concatenate stringsIn Elm, the view function is called every time the model changes so it is here that we change the style to move the plate of sushi using the Virtual DOM. In Javascript we modify the DOM directly
In Elm we need to convert types because it is a strictly typed language (String.fromFloat), Javascript does it automatically
{ scale, x }
it is a way to deconstruct a record directly. In realitychangeStyle
gets only one argument. Arguments in Elm functions are separated by spaces, not commas
Elm Records vs. Javascript Objects
-- Elm
onTheKaiten =
{ x = 50
, scale = 1
}
inTheKitchen =
{ x = 600
, scale = 0
}
init =
{ currentState = onTheKaiten
, animationStart = onTheKaiten
, target = onTheKaiten
, animationLength = 0
, progress = Nothing
}
// Javascript
onTheKaiten = {
x: 50,
scale: 1
};
inTheKitchen = {
x: 600,
scale: 0
};
init = {
currentState: onTheKaiten,
animationStart: onTheKaiten,
target: onTheKaiten,
animationLength: 0,
progress: null
}
model = init
In Elm, we use
=
instead of:
. Also usually commas are at the beginning so that they are aligned vertically and the code seems tidierModel
in Elm contains the entire state of the application. It is a single source of truth enforced by the compiler and is immutable. I use a global model object in Javascript just to make the code look similar, but it carries different meaning. In Javascript it is just a mutable global object
The calculateDelta function
// Javascript
previousAnimationFrame = null;
function calculateDelta(timestamp) {
var delta = null;
if (model.progress === 0) {
delta = 1000 / 60;
previousAnimationFrame = timestamp;
} else {
delta = timestamp - previousAnimationFrame;
previousAnimationFrame = timestamp;
}
return delta;
}
This is some boilerplate needed only on the Javascript side because in Elm the delta is coming from the Elm Runtime
This function determine the amount of time (delta) passed between each animation frame
The clickOnPage Function
-- Elm
clickOnPage model =
if model.target == onTheKaiten then
{ model
| target = inTheKitchen
, animationStart = model.currentState
, animationLength = 1000
, progress = Just 0
}
else
{ model
| target = onTheKaiten
, animationStart = model.currentState
, animationLength = 1000
, progress = Just 0
}
// Javascript
clickOnPage = function() {
if (model.target === onTheKaiten) {
model = {
...model,
target: inTheKitchen,
animationStart: model.currentState,
animationLength: 1000,
progress: 0,
}
window.requestAnimationFrame(animationFrame);
} else {
model = {
...model,
target: onTheKaiten,
animationStart: model.currentState,
animationLength: 1000,
progress: 0
}
window.requestAnimationFrame(animationFrame);
}
};
In Elm all functions are pure so can only rely on input arguments. This is why we are passing the model. In the Javascript example we made โmodelโ global so we donโt need to pass around
Also the syntax
{ model | a = b }
is used to copy a record changing only the value of keya
intob
. We need to copy records as it is not possible to change them in place.model.a = b
is not a valid construct. All data is immutable in ElmIn Elm, requestAnimationFrame is handled in different places. It is activated in subscriptions when
progress
becomesJust 0
. In Javascript we just call it from here
The animationFrame function
-- Elm
animationFrame model delta =
case model.progress of
Just progress ->
if progress < model.animationLength then
let
animationRatio =
Basics.min 1 (progress / model.animationLength)
newX =
model.animationStart.x
+ (model.target.x - model.animationStart.x)
* animationRatio
newScale =
model.animationStart.scale
+ (model.target.scale - model.animationStart.scale)
* animationRatio
in
{ model
| progress = Just <| progress + delta
, currentState = { x = newX, scale = newScale }
}
else
{ model
| progress = Nothing
, currentState = model.target
}
Nothing ->
model
// Javascript
function animationFrame(timestamp) {
if (model.progress !== null) {
if (model.progress < model.animationLength) {
var delta = calculateDelta(timestamp);
var animationRatio =
Math.min(1, model.progress / model.animationLength);
var newX =
model.animationStart.x +
(model.target.x - model.animationStart.x) *
animationRatio;
var newScale =
model.animationStart.scale +
(model.target.scale - model.animationStart.scale) *
animationRatio;
model = { ...model,
progress: model.progress + delta,
currentState: { x: newX, scale: newScale }
}
changeStyle(newScale, newX);
window.requestAnimationFrame(animationFrame);
} else {
model = { ...model,
progress: null,
currentState: model.target
}
}
}
}
This is the function that recalculates the new position of the sushi plate. Similar on both sides. The Javascript version needs to change the style calling
changeStyle
while this is handled in the view by ElmAlso Javascript needs to call
requestAnimationFrame
at the end, so that the animation keeps goingJavascript is done
Extra Elm stuff
From there there is the Elm code that wire everything together.
The subscriptions
-- Elm
subscriptions model =
case model.progress of
Just _ ->
Browser.Events.onAnimationFrameDelta AnimationFrame
Nothing ->
Sub.none
- Here is where we tell the Elm runtime when or when no to send messages on the animation frame
The update function
-- Elm
update msg model =
case msg of
ClickOnPage ->
clickOnPage model
AnimationFrame delta ->
animationFrame model delta
- Here we explain what to do when we receive messages.
The Types
-- Elm
type Msg
= AnimationFrame Float
| ClickOnPage
type alias State =
{ scale : Float, x : Float }
type alias Model =
{ currentState : State
, target : State
, animationLength : Float
, progress : Maybe Float
, animationStart : State
}
- Type definitions
The Elm Runtime entry point
-- Elm
main : Program () Model Msg
main =
sandboxWithTitleAndSubscriptions
{ title = title
, init = init
, view = view
, update = update
, subscriptions = subscriptions
Connecting everything to the Elm Runtime using the custom entry point
sandboxWithTitleAndSubscriptions
. Elm provides by default four entry-points (sandbox
,element
,document
andapplication
) in order of complexityWhat we need for the animation is a combination of those, so I created
sandboxWithTitleAndSubscriptions
. It is similar tosandbox
but with some extra stuff
The sandboxWithTitleAndSubscriptions
-- Elm
sandboxWithTitleAndSubscriptions args =
Browser.document
{ init = \_ -> ( args.init, Cmd.none )
, view = \model -> { title = args.title, body = args.view model }
, update = \msg model -> ( args.update msg model, Cmd.none )
, subscriptions = args.subscriptions
}
- This is the custom defined entry point
Conclusion
I feel that Elm and Javascript are not that different after all, from a syntax point of view. I hope this post helps to make things less scary.
The Code
Elm version: Editable Demo, Demo, Code
Javascript version: Editable Demo, Demo, Code
Related Links
Side-by-side mappings between JavaScript and Elm
The Elm Minimal Syntax Reference
A SSCCE (Short, Self Contained, Correct (Compilable), Example) for the entire Elm syntax by pdamoc
And its Ellie version
Top comments (0)