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 }