Yesterday I started to implement the logic for a simple calculator web app that I'm recreating.

One of my goals in building it is to use the features of Elm to cleanly separate the application logic from the UI.

I believe I achieved what I was going for and that's why I'm so excited to share my progress with you.

So what did I do?

I made a data structure in a Calculator module with the following public API:



module Calculator exposing ( Calculator , Key ( .. ) , Operator ( .. ) , new , process , Display , toDisplay )

N.B. The implementation details aren't important right now.

Calculator is an opaque type.

The new function creates a Calculator . process takes a Key and a Calculator and returns an updated Calculator . And, toDisplay allows you to observe the results produced by a given Calculator .

For e.g.



Calculator . new |> Calculator . process ( Digit 1 ) |> Calculator . process ( Digit 2 ) |> Calculator . process ( Operator Plus ) |> Calculator . process ( Digit 3 ) |> Calculator . toDisplay { expr = " 12+3" , output = " 3" }

I experienced two major benefits. The first was that I was able to unit test (yes, testing is important even in a typed functional language) my logic and gain confidence in my implementation.

Here are the tests:



module Test . Calculator exposing ( suite ) import Expect import Test exposing ( .. ) import Calculator exposing ( Key ( .. ) , Operator ( .. )) suite : Test suite = describe " Calculator" [ processSuite ] processSuite : Test processSuite = describe " process" [ describe " when nothing has been entered" <| let calculator = Calculator . new in [ test " pressing AC" <| \ _ -> calculator |> Calculator . process AC |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing a digit" <| \ _ -> calculator |> Calculator . process ( Digit 1 ) |> Calculator . toDisplay |> Expect . equal { expr = " 1" , output = " 1" } , test " pressing an operator" <| \ _ -> calculator |> Calculator . process ( Operator Plus ) |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing =" <| \ _ -> calculator |> Calculator . process Equal |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } ] , describe " when a number has been entered" <| let calculator = Calculator . new |> Calculator . process ( Digit 1 ) |> Calculator . process ( Digit 2 ) in [ test " pressing AC" <| \ _ -> calculator |> Calculator . process AC |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing a digit" <| \ _ -> calculator |> Calculator . process ( Digit 3 ) |> Calculator . toDisplay |> Expect . equal { expr = " 123" , output = " 123" } , test " pressing an operator" <| \ _ -> calculator |> Calculator . process ( Operator Plus ) |> Calculator . toDisplay |> Expect . equal { expr = " 12+" , output = " +" } , test " pressing =" <| \ _ -> calculator |> Calculator . process Equal |> Calculator . toDisplay |> Expect . equal { expr = " 12=12" , output = " 12" } ] , describe " when a number and operator has been entered" <| let calculator = Calculator . new |> Calculator . process ( Digit 1 ) |> Calculator . process ( Digit 2 ) |> Calculator . process ( Operator Plus ) in [ test " pressing AC" <| \ _ -> calculator |> Calculator . process AC |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing a digit" <| \ _ -> calculator |> Calculator . process ( Digit 3 ) |> Calculator . toDisplay |> Expect . equal { expr = " 12+3" , output = " 3" } , test " pressing an operator" <| \ _ -> calculator |> Calculator . process ( Operator Minus ) |> Calculator . toDisplay |> Expect . equal { expr = " 12-" , output = " -" } , test " pressing =" <| \ _ -> calculator |> Calculator . process Equal |> Calculator . toDisplay |> Expect . equal { expr = " 12=12" , output = " 12" } ] , describe " when a complete expression has been entered" <| let calculator = Calculator . new |> Calculator . process ( Digit 1 ) |> Calculator . process ( Digit 2 ) |> Calculator . process ( Operator Plus ) |> Calculator . process ( Digit 3 ) |> Calculator . process ( Operator Minus ) |> Calculator . process ( Digit 4 ) in [ test " pressing AC" <| \ _ -> calculator |> Calculator . process AC |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing a digit" <| \ _ -> calculator |> Calculator . process ( Digit 5 ) |> Calculator . toDisplay |> Expect . equal { expr = " 12+3-45" , output = " 45" } , test " pressing an operator" <| \ _ -> calculator |> Calculator . process ( Operator Plus ) |> Calculator . toDisplay |> Expect . equal { expr = " 12+3-4+" , output = " +" } , test " pressing =" <| \ _ -> calculator |> Calculator . process Equal |> Calculator . toDisplay |> Expect . equal { expr = " 12+3-4=11" , output = " 11" } ] , describe " when an answer is given" <| let calculator = Calculator . new |> Calculator . process ( Digit 1 ) |> Calculator . process ( Digit 2 ) |> Calculator . process ( Operator Plus ) |> Calculator . process ( Digit 3 ) |> Calculator . process ( Operator Minus ) |> Calculator . process ( Digit 4 ) |> Calculator . process Equal in [ test " pressing AC" <| \ _ -> calculator |> Calculator . process AC |> Calculator . toDisplay |> Expect . equal { expr = " " , output = " 0" } , test " pressing a digit" <| \ _ -> calculator |> Calculator . process ( Digit 9 ) |> Calculator . toDisplay |> Expect . equal { expr = " 9" , output = " 9" } , test " pressing an operator" <| \ _ -> calculator |> Calculator . process ( Operator Plus ) |> Calculator . toDisplay |> Expect . equal { expr = " 11+" , output = " +" } , test " pressing =" <| \ _ -> calculator |> Calculator . process Equal |> Calculator . toDisplay |> Expect . equal { expr = " 12+3-4=11" , output = " 11" } ] ]

The second benefit was how trivial it became to implement the UI. I see many web apps that write extensive integration tests for UI code but that's because they couple the UI with the application logic. With proper separation of concerns you'd end up with more unit tests and much less integration tests (if any).

Here's the code that makes the UI work:



-- MODEL type alias Model = { calculator : Calculator } init : Model init = { calculator = Calculator . new } -- UPDATE type Msg = Clicked Key update : Msg -> Model -> Model update msg model = case msg of Clicked key -> { model | calculator = Calculator . process key model . calculator } -- VIEW viewCalculator : Calculator -> Html Msg viewCalculator calculator = let display = Calculator . toDisplay calculator in div [ class " calculator" ] [ div [ class " calculator__expr" ] [ if String . isEmpty display . expr then text ( String . fromChar nonBreakingSpace ) else text display . expr ] , div [ class " calculator__output" ] [ text display . output ] , div [ class " calculator__buttons" ] [ button [ class " r0 c0 colspan2 bg-red" , onClick ( Clicked AC ) ] [ text " AC" ] , button [ class " r0 c2" , disabled True ] [ text " ÷" ] , button [ class " r0 c3" , onClick ( Clicked ( Operator Times )) ] [ text " ×" ] , button [ class " r1 c0" , onClick ( Clicked ( Digit 7 )) ] [ text " 7" ] , button [ class " r1 c1" , onClick ( Clicked ( Digit 8 )) ] [ text " 8" ] , button [ class " r1 c2" , onClick ( Clicked ( Digit 9 )) ] [ text " 9" ] , button [ class " r1 c3" , onClick ( Clicked ( Operator Minus )) ] [ text " -" ] , button [ class " r2 c0" , onClick ( Clicked ( Digit 4 )) ] [ text " 4" ] , button [ class " r2 c1" , onClick ( Clicked ( Digit 5 )) ] [ text " 5" ] , button [ class " r2 c2" , onClick ( Clicked ( Digit 6 )) ] [ text " 6" ] , button [ class " r2 c3" , onClick ( Clicked ( Operator Plus )) ] [ text " +" ] , button [ class " r3 c0" , onClick ( Clicked ( Digit 1 )) ] [ text " 1" ] , button [ class " r3 c1" , onClick ( Clicked ( Digit 2 )) ] [ text " 2" ] , button [ class " r3 c2" , onClick ( Clicked ( Digit 3 )) ] [ text " 3" ] , button [ class " r3 c3 rowspan2 bg-blue" , onClick ( Clicked Equal ) ] [ text " =" ] , button [ class " r4 c0 colspan2" , onClick ( Clicked ( Digit 0 )) ] [ text " 0" ] , button [ class " r4 c2" , disabled True ] [ text " ." ] ] ]

Key takeaway: Let the UI drive a data structure and unit test the living daylights out of that data structure.

What do you think about this approach to implementing more complicated UIs?