Elm: Dependency Injection and Functional Programming

Intro

In this post, I will attempt to document my understanding of how to do Dependency Injection within Elm.

Dependency Injection is a technique used to provide a set of dependencies that a system requires at runtime. This technique is especially advantageous when testing that the system complies with business rules without having to provide all of the dependencies that a production environment may require.

Function Interface

To support Dependency Injection within a functional programming paradigm, we can create a type to represent the interface to a function. To accomplish this, we need to specify the signature of the arbitrary function.


Here’s a type called LoginFunction within the Domain.Core module along with its signature:

type alias Loginfunction =
    Login.Model -> Login.Model

We can then use this type as an interface to a function. Hence, we can pass a function as an argument to the tryLogin function as well as other parameters.

Here’s an example:

tryLogin : Login.Loginfunction -> String -> String -> Login.Model
tryLogin loginf username password =
    loginf <| Login.Model username password False

Test API

The TestAPI is a server library that is designed to support testing by executing client requests. These requests can be for serving up some arbitrary value to the client or by executing some arbitrary operation that adheres to an interface that the client is required to comply with.

Here’s the implementation of the TestAPI’s tryLogin function:

module Tests.TestAPI exposing (..)

import Controls.Login as Login exposing (Model)

tryLogin : Login.Model -> Login.Model
tryLogin credentials =
    let
        successful =
            String.toLower credentials.username == "test" && String.toLower credentials.password == "test"
    in
        if successful then
            { username = credentials.username, password = credentials.password, loggedIn = True }
        else
            { username = credentials.username, password = credentials.password, loggedIn = False }

Registering Dependencies

We can engineer our webpage to have the capability to run in either isolation or integration mode. Isolation mode means that our webpage will not rely on system integration (i.e. web server) to function. Hence, our webpage is its own self-contained system. Integration mode, on the other hand, means that our system does require external systems in order for the webpage to be functional.

Here’s some of the code to do it:

import Domain.Core exposing (..)
import Controls.Login as Login exposing (..)
import Tests.TestAPI as TestAPI exposing (tryLogin)
import Services.Server as Services exposing (tryLogin)
...
-- CONFIGURATION

configuration : Configuration
configuration =
    Isolation -- SET CONFIGURATION HERE ! ! !

type Configuration
    = Integration
    | Isolation

type alias Dependencies =
    { tryLogin : Login.Loginfunction }

runtime : Dependencies
runtime =
    case configuration of
        Integration ->
            Dependencies Services.tryLogin

        Isolation ->
            Dependencies TestAPI.tryLogin

The client code was updated with the following call to the tryLogin function:

        OnLogin subMsg ->
            case subMsg of
                Login.Attempt v ->
                    let
                        latest =
                            Login.update subMsg model.login
                    in
                        { model | login = runtime.tryLogin latest }

Conclusion

In conclusion, I attempted to document my journey of learning Elm by exploring Dependency Injection. The code can be found on GitHub.

Appendix

Below, are the modules that I implemented.

Home.elm

module Home exposing (..)

import Domain.Core exposing (..)
import Controls.Login as Login exposing (..)
import Tests.TestAPI as TestAPI exposing (tryLogin)
import Services.Server as Services exposing (tryLogin)
import Html exposing (..)
import Html.Attributes exposing (..)

main =
    Html.beginnerProgram
        { model = model
        , update = update
        , view = view
        }

-- CONFIGURATION

configuration : Configuration
configuration =
    Isolation

type Configuration
    = Integration
    | Isolation

type alias Dependencies =
    { tryLogin : Login.Loginfunction }

runtime : Dependencies
runtime =
    case configuration of
        Integration ->
            Dependencies Services.tryLogin

        Isolation ->
            Dependencies TestAPI.tryLogin

-- MODEL

type alias Model =
    { videos : List Video
    , articles : List Article
    , login : Login.Model
    }

model : Model
model =
    { videos = [], articles = [], login = Login.model }

init : ( Model, Cmd Msg )
init =
    ( model, Cmd.none )

-- UPDATE

type Msg
    = Video Video
    | Article Article
    | Submitter Submitter
    | Search String
    | Register
    | OnLogin Login.Msg

update : Msg -> Model -> Model
update msg model =
    case msg of
        Video v ->
            model

        Article v ->
            model

        Submitter v ->
            model

        Search v ->
            model

        Register ->
            model

        OnLogin subMsg ->
            case subMsg of
                Login.Attempt v ->
                    let
                        latest =
                            Login.update subMsg model.login
                    in
                        { model | login = runtime.tryLogin latest }

                Login.UserInput _ ->
                    { model | login = Login.update subMsg model.login }

                Login.PasswordInput _ ->
                    { model | login = Login.update subMsg model.login }

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ header []
            [ label [] [ text "Nikeza" ]
            , model |> sessionUI
            ]
        , footer [ class "copyright" ]
            [ label [] [ text "(c)2017" ]
            , a [ href "" ] [ text "GitHub" ]
            ]
        ]

sessionUI : Model -> Html Msg
sessionUI model =
    let
        loggedIn =
            model.login.loggedIn

        welcome =
            p [] [ text <| "Welcome " ++ model.login.username ++ "!" ]

        signout =
            a [ href "" ] [ label [] [ text "Signout" ] ]
    in
        if (not loggedIn) then
            Html.map OnLogin <| Login.view model.login
        else
            div [ class "signin" ] [ welcome, signout ]

Core.elm

module Domain.Core exposing (..)

import Controls.Login as Login exposing (Model, Loginfunction)

type Submitter
    = Submitter String

type Title
    = Title String

type Url
    = Url String

type Video
    = Video Post

type Article
    = Article Post

type alias Post =
    { submitter : Submitter, title : Title, url : Url }

type alias Loginfunction =
    Login.Model -> Login.Model

tryLogin : Login.Loginfunction -> String -> String -> Login.Model
tryLogin loginf username password =
    loginf <| Login.Model username password False

Login.elm

module Controls.Login exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)

-- MODEL

type alias Model =
    { username : String
    , password : String
    , loggedIn : Bool
    }

model : Model
model =
    Model "" "" False

-- UPDATE

type Msg
    = UserInput String
    | PasswordInput String
    | Attempt ( String, String )

update : Msg -> Model -> Model
update msg model =
    case msg of
        UserInput v ->
            { model | username = v }

        PasswordInput v ->
            { model | password = v }

        Attempt ( username, password ) ->
            { model | username = username, password = password }

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input [ class "signin", type_ "submit", value "Signin", onClick <| Attempt ( model.username, model.password ) ] []
        , input [ class "signin", type_ "password", placeholder "password", onInput PasswordInput, value model.password ] []
        , input [ class "signin", type_ "text", placeholder "username", onInput UserInput, value model.username ] []
        ]

TestAPI.elm

module Tests.TestAPI exposing (..)

import Controls.Login as Login exposing (Model)

tryLogin : Login.Model -> Login.Model
tryLogin credentials =
    let
        successful =
            String.toLower credentials.username == "test" && String.toLower credentials.password == "test"
    in
        if successful then
            { username = credentials.username, password = credentials.password, loggedIn = True }
        else
            { username = credentials.username, password = credentials.password, loggedIn = False }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: