Elm vs. Javascript: Side by Side Code Comparison

lucamug

lucamug

Posted on May 14, 2020

Elm vs. Javascript: Side by Side Code Comparison

Alt Text

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 "🍣" ]
    ]
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode
  • 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 code

  • The Elm view needs the text to be the argument of the text function. We cannot just put it there similar to HTML or JSX

  • In 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 or changeStyle for example). Actually in Elm more than changeStyle is rather generateStyle

  • 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")
    ]
Enter fullscreen mode Exit fullscreen mode
// Javascript

function changeStyle(scale, x) {
    sushi.style.transform = "scale(" + scale + ")";
    sushi.style.left = x + "px";
}
Enter fullscreen mode Exit fullscreen mode
  • ++ vs. + to concatenate strings

  • In 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 reality changeStyle 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
    }
Enter fullscreen mode Exit fullscreen mode
// Javascript

onTheKaiten = {
    x: 50,
    scale: 1
};

inTheKitchen = {
    x: 600,
    scale: 0
};

init = {
    currentState: onTheKaiten,
    animationStart: onTheKaiten,
    target: onTheKaiten,
    animationLength: 0,
    progress: null
}

model = init
Enter fullscreen mode Exit fullscreen mode
  • In Elm, we use = instead of :. Also usually commas are at the beginning so that they are aligned vertically and the code seems tidier

  • Model 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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
        }
Enter fullscreen mode Exit fullscreen mode
// 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);
    }
};
Enter fullscreen mode Exit fullscreen mode
  • 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 key a into b. 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 Elm

  • In Elm, requestAnimationFrame is handled in different places. It is activated in subscriptions when progress becomes Just 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
Enter fullscreen mode Exit fullscreen mode
// 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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 Elm

  • Also Javascript needs to call requestAnimationFrame at the end, so that the animation keeps going

  • Javascript 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
    }
Enter fullscreen mode Exit fullscreen mode
  • Type definitions

The Elm Runtime entry point

-- Elm

main : Program () Model Msg
main =
    sandboxWithTitleAndSubscriptions
        { title = title
        , init = init
        , view = view
        , update = update
        , subscriptions = subscriptions     
Enter fullscreen mode Exit fullscreen mode
  • Connecting everything to the Elm Runtime using the custom entry point sandboxWithTitleAndSubscriptions. Elm provides by default four entry-points (sandbox, element, document and application) in order of complexity

  • What we need for the animation is a combination of those, so I created sandboxWithTitleAndSubscriptions. It is similar to sandbox 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
        }
Enter fullscreen mode Exit fullscreen mode
  • 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

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

đź’– đź’Ş đź™… đźš©
lucamug
lucamug

Posted on May 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related