Dat functioneel programmeren steeds populairder wordt, kunnen we niet meer ontkennen. Functionele code heeft voordelen vergeleken met object georiënteerd programmeren, zoals betere testbaarheid, geen neveneffecten en makkelijker concurrent programmeren. Scala maakt functioneel programmeren erg aantrekkelijk, zelfs in Java wordt het steeds eenvoudiger door bijvoorbeeld het gebruik van lambda functies.
Ook front-end toepassingen kunnen in functioneel JavaScript geschreven worden. JavaScript frameworks en libraries, zoals React en Angular, ondersteunen steeds meer functionele features. Toch hebben zij allen een groot probleem: ‘undefined is not a function’. Bestaat een oplossing voor front-end development waarmee dit niet voor kan komen? Ontdek Elm.
Auteurs: Deniz Turan is een Java Developer bij JCore B.V. met een passie voor full-stack development en technologie. Anne van den Berg is een gepassioneerde Java Developer bij JCore B.V. met tevens een sterke interesse in front end programmeren
Elm is een taal die compileert naar JavaScript, HTML, en CSS. Hierdoor kan het in elke browser draaien. Het heeft een aantal krachtige eigenschappen:
– het is een pure, functionele taal;
– het is statically typed (alle types zijn compile-time bekend);
– het heeft een vriendelijke compiler;
– het heeft een virtuele DOM.
Als je al bekend bent met Haskell, dan zal Elm er niet onbekend uitzien: het is vooral geïnspireerd door Haskell. Sterker nog, de Elm compiler is geschreven in Haskell.
Indien je als back-end ontwikkelaar begint met front-end ontwikkelen, kan dit laatste aanvoelen als een mijnenveld (runtime exceptions) die je moet ontwijken. Java is behoorlijk type safe, JavaScript en bijbehorende libraries zijn dat niet. De komst van TypeScript en Flow maken het ontwikkelen van front-end applicaties een stuk veiliger, maar niet waterdicht.
Elm is een pure, functionele taal. Dit betekent dat de output van een functie alleen wordt bepaald door de input van de functie en het geen side effects heeft, zoals het lezen of schrijven van state buiten de scope de functie. Dat betekent ook dat alle waardes immutable zijn en dat alles een type heeft. Sterker nog, de types moeten bekend zijn tijdens het compileren. In de praktijk betekent dit dat de ontwikkelaar elke mogelijke situatie moet afvangen in de code. Hierdoor kunnen geen runtime exceptions optreden. Uiteraard kan de business logica nog fouten bevatten, maar errors zoals ‘undefined is not a function’ zijn verleden tijd.
Het bovenstaande wordt mede mogelijk gemaakt door de krachtige compiler. Wat de compiler zo vriendelijk maakt, zijn de errors. Deze kunnen wel voorkomen in tegenstelling tot runtime exceptions. Want als een bepaald type niet bekend is of verkeerd gebruikt wordt, compiled het simpelweg niet. Compile errors zijn uitgebreid beschreven en komen tevens met een suggestie en hints zoals in listing 1.
Dit gaat natuurlijk iets verder dan een type die niet correct is. De compiler probeert namelijk bij alle errors te achterhalen wat de oorzaak kan zijn en komt vaak met nuttige tips, zoals in dit geval een suggestie dat het mogelijk om een typo gaat.
Het belangrijkste om te onthouden over Elm is dat letterlijk alles een functie is. Operators (`+`, `-`, `/` etc.) zijn functies, net als constantes (functies zonder argument), if-else expressies en de HTML en CSS die in Elm geschreven worden. Alle functies in Elm hebben maximaal één parameter. Een functie die meerdere parameters nodig heeft, is in feite een functie die een functie returned die de tweede parameter accepteert. Dit process heet currying, een techniek dat in functionele talen veel gebruikt wordt.
addNumbers : Int -> Int -> Int
addNumbers a b =
a + b
De functie `addNumbers` in listing 2 accepteert een Int, geeft vervolgens een functie terug die een Int als parameter heeft met een Int als resultaat. Links van de pijl is altijd de parameter en rechts het resultaat. Dus in feite staat er `Int -> (Int -> Int)`. Omdat alle functies zo werken, kan een nieuwe functie gemaakt worden door `addNumbers` aan te roepen met één parameter (zie listing 3). Op deze manier is het gemakkelijker een functie op te splitsen in kleinere herbruikbare functies.
addTwo : Int -> Int
addTwo =
addNumbers 2
Elm kent geen objecten, maar records. In listing 4 is te zien hoe het record ‘Employee’ gedefinieerd wordt. Voor het initialiseren van een record zijn alle velden verplicht. Het record Employee kan als volgt worden geïnitialiseerd: ‘Employee John 4000’. Net als bij functies worden de verschillende argumenten gescheiden door spaties. Bovendien kunnen generics toegepast worden in Elm door het generic type te scheiden d.m.v. een spatie in de definitie van een functie of record, zie bijvoorbeeld ‘List Employee’ in listing 5.
type alias Employee =
{ name : String
, salary : Int
}
type alias Company =
{ name : String
, employees : List Employee
}
Opvallend is ook de bijzondere formatting van de code. Deze code style heeft als doel om de code zo leesbaar mogelijk te maken. Meer informatie over de code style is te vinden in de style guide van Elm: http://elm-lang.org/docs/style-guide
Voor het gemak bestaat de tool ‘elm-format’ die de code herformatteert wanneer een elm-bestand wordt opgeslagen. Het mooie aan ‘elm-format’ is dat deze niet te configureren is en dat vrijwel iedereen het gebruikt. Daardoor is de formatting consistent voor alle packages die beschikbaar zijn.
We hebben een kleine voorbeeldapplicatie geschreven die we in dit artikel zullen toelichten. In listing 6 is de folder structuur weergegeven van onze Java Magazine App. De elm.json is een bestand waarin alle dependencies staan, vergelijkbaar met package.json van npm. De elm-stuff folder is waar gecompileerde Elm modules terecht komen. De project dependencies worden op een globaal niveau opgeslagen, zodat deze niet voor ieder project opnieuw gedownload hoeven te worden.
In onze app kunnen auteurs toegevoegd worden voor een Java Magazine artikel. Informatie over de auteurs wordt bij Gravatar opgevraagd, welke een API heeft waar een avatar en wat profielgegevens kunnen worden opgehaald aan de hand van een emailadres.
Een Elm applicatie is opgebouwd uit een Model, Update en een View. Dit is vergelijkbaar met het Model-View-Controller pattern. Het Model is de huidige staat van de applicatie, in Update wordt deze state bijgewerkt en in de View is beschreven hoe de state wordt weergegeven. De View kan mogelijk een update triggeren als bijvoorbeeld op een knop wordt geklikt.
type alias AuthorRecord =
{ displayName : String
, aboutMe : String
, currentLocation : String
, thumbnailUrl : String
}
type alias Model =
{ newAuthorEmail : String
, authors : List AuthorRecord
}
init : () -> ( Model, Cmd Msg )
init _ =
( Model “” [], Cmd.none )
In listing 7 zijn de types te zien en hoe deze geïnitialiseerd worden. We hebben een AuthorRecord om data van het Gravatar profiel in op te slaan en een Model met de state van onze applicatie. Deze bevat een emailadres van de persoon die toegevoegd moet worden en een lijst van alle toegevoegde Gravatar profielen. De init functie geeft de initiële state van onze applicatie, namelijk een Model met een lege String en een lege lijst. We initialiseren ook met een Cmd.none, hier komen we straks nog op terug.
De update is de functie die de state bijwerkt door een nieuwe versie van de state te returnen. Het is een nieuwe versie omdat het immutable is. Daarnaast wordt er een `Msg` gedefinieerd welke aangeeft hoe de state gewijzigd mag worden. Deze acties kun je zien in listing 8.
type Msg
= AuthorEmail String
| AddAuthor
| GravatarProfile (Result Http.Error AuthorRecord)
Onze applicatie kent drie verschillende acties. Een actie met de type String om het nieuwe emailadres op te slaan in de state, een actie om het profiel van de auteur op te halen en een actie om het resultaat van de vorige actie te verwerken. Wat opvalt, is dat de ‘GravatarProfile’ actie een resultaat van ‘Http.Error’ of ‘AuthorRecord’ heeft.
In listing 9 is de implementatie van onze update-functie te zien. De update-functie krijgt de ‘Msg’ binnen om te bepalen welke actie uitgevoerd moet worden en een ‘Model’ om de huidige staat mee te geven. Het uiteindelijke resultaat is hetzelfde type als onze init: een Model en een Cmd van het type Msg. Interessant is dat Elm ons forceert om tevens de error af te vangen, omdat het onderdeel is van de GravatarProfile msg. Het compiled simpelweg niet indien dit niet wordt gedaan.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
AuthorEmail newEmail ->
( { model | newAuthorEmail = newEmail }, Cmd.none )
AddAuthor ->
( model, getGravatarProfile model.newAuthorEmail )
GravatarProfile (Ok authorProfile) ->
( { model | authors = authorProfile :: model.authors }
, Cmd.none
)
GravatarProfile (Err _) ->
— Normally you would show some sort of error to the user
( model, Cmd.none )
Bij de ‘AuthorEmail’ implementatie staat ‘{ model | newAuthorEmail = newEmail }’. Dit maakt een nieuw Model aan op basis van het huidige Model ‘model’ en verandert alleen de property ‘newAuthorEmail’. Zo kan gemakkelijk een kopie gemaakt worden van een Record. Hetzelfde is te zien bij de implementatie van ‘GravatarProfile’. Alleen hier is ‘authors’ een nieuwe lijst waar authorProfile vooraan wordt toegevoegd aan de bestaande lijst van auteurs door middel van de operator ‘::’.
Tot slot hebben we het toevoegen van een auteur (‘AddAuthor’). Die roept de functie ‘getGravatarProfile’ aan welke een http-request uitvoert. Het resultaat is een Cmd van het type Msg, namelijk de Msg ‘GravatarProfile’. The Cmd staat voor command, dit wordt door Elm gebruikt voor side effects, zoals het lezen van een bestand of het uitvoeren van een http request. De Cmd is een manier om Elm te vertellen dat iets buiten de applicatie moet worden gedaan. Elm ontvangt runtime de Cmd. Het resultaat kan alleen via de ‘Msg’ terugkomen. De Cmd.none in de init is dus nodig, omdat geen http-request wordt uitgevoerd totdat de Msg ‘AddAuthor’ wordt aangeroepen.
In listing 10 is een gedeelte van de view-functie te zien. Zoals we eerder schreven, is dit geen HTML, maar zijn het functies die HTML representeren. Het type van de eerste parameter in de functie is Model. Het resultaat is Html van het type Msg, wat gelijk de link is met de update-functie. Zo is te zien dat de ‘AuthorEmail’ wordt afgevuurd bij het ‘onInput’ event. Hetzelfde geldt voor de button, waar de ‘onClick’ het ‘AddAuthor’ Msg triggert.
view : Model -> Html Msg
view model =
div []
[ h2 [] [ text “Add authors to your Java Magazine article” ]
, input [ placeholder “Author Email address”, onInput AuthorEmail ] []
, button [ onClick AddAuthor ] [ text “Add Author” ]
]
Nu vraag je je misschien af, hoe kan alles typed zijn als een http request wordt uitgevoerd? Dit kan doordat Elm je forceert aan te geven wat het type is van het resultaat van de http request. Bij JSON data, zoals in ons geval, kun je een Decoder schrijven om Elm te vertellen wat voor type je verwacht.
getGravatarProfile : String -> Cmd Msg
getGravatarProfile profileEmail =
Http.send GravatarProfile
(Http.get (createProfileUrl profileEmail) decodeGravatarResponse)
decodeGravatarResponse : Decoder AuthorRecord
decodeGravatarResponse =
let
authorDecoder =
Json.Decode.succeed AuthorRecord
|> JsonPipeline.required “displayName” string
|> JsonPipeline.optional “aboutMe” string “-”
|> JsonPipeline.optional “currentLocation” string “-”
|> JsonPipeline.required “thumbnailUrl” string
in
at [ “entry”, “0” ] authorDecoder
In listing 11 is de implementatie van de ‘getGravatarProfile’ functie weergegeven. Het Msg GravatarProfile wordt meegegeven aan de ‘Http.send’ functie. De decoder-functie wordt ook meegegeven. Deze zet het JSON resultaat om naar een AuthorRecord. De variabelen displayName en thumbnailUrl zijn verplicht en dit wordt ook gecontroleerd door Elm. Als deze niet aanwezig zijn of het decoden mislukt, dan wordt de GravatarProfile Msg afgevuurd met een error. De optionele waardes hebben in dit geval een default value van “-”.
Ondanks dat we in onze app geen styling hebben, heeft Elm uiteraard ondersteuning voor styles. Zo zijn alle CSS properties beschikbaar als functies in de package ‘rtfeldman/elm-css’. Er is ook een alternatieve manier, zodat je niet met CSS werkt, maar met een library die een sterke onderscheid maakt tussen layout en styling, genaamd Elm UI (in de package ‘mdgriffith/elm-ui’). Desalniettemin kan ook een externe CSS-bestand geladen worden.
Alhoewel Elm in eerste instantie er wat vreemd uitziet, is het eigenlijk een bijzonder krachtige programmeertaal. Een taal waar zelfs http-calls typed zijn. Omdat alles zo strikt is, worden een hoop fouten al voor je afgevangen en word je geforceerd om iets te doen met alle mogelijke situaties in de code. Hierdoor zijn runtime errors dus verleden tijd.
De volledige applicatie die we voor dit artikel gebouwd hebben, kun je vinden op GitHub: https://github.com/bergac/elm-gravatar-app. Daar staan ook instructies bij hoe je de applicatie zelf kunt testen. Als je meer wilt weten over Elm, dan is https://guide.elm-lang.org/ een goede beginplek. Hierin staan een hoop voorbeelden met uitleg. Er is ook een online editor waar je Elm apps mee kunt schrijven: Ellie App (https://ellie-app.com). Ellie App heeft de mogelijkheid om de packages te beheren en debug / console logs in te zien.
Elm heeft ook een behulpzame community die graag beginners helpen, dat kan bijvoorbeeld via Slack of de Elm Discourse.