Some time ago I was able to listen to the episode We’re Teaching Functional Programming Wrong of the podcast Corecursive. Richard Feldman tells of a purely functional language with which you can actually create dynamic web pages. It is not even necessary to understand what a Monad is. Since I consider object-oriented programming to be overrated*The talk [Free your Functions](https://www.youtube.com/watch?v=WLDT1lDOsb4) by Klaus Iglberger highlights some practical aspects my point of view., I had a closer look at this approach. In this post I will report on my first steps, hurdles and progress in Elm and focus on aspects that were unfamiliar to me having an imperative background.
Elm installation and development environment
You can, e.g., use the package manager Npm like me or alternatively download binaries. As an IDE I use VS Code. I also installed elm-format
via Npm so that VS Code can format my source code via Shift-Alt-F. With
elm make src/File.elm --output app.js
you compile an Elm source file to JavaScript. The product can be integrated into an HTML page via
<div id="app"></div>
<script src="app.js"></script>
<script>
Elm.Calc.init({node: document.getElementById("app")});
</script>
Private retirement provision
Since private pension provision is currently an issue for us, I built a calculator as a mini test project. You pass the annual interest rate, the monthly savings rate, and the number of savings years and it spits out the account balance at the end of the savings period. You can see the result in the following box, which I have included here using <iframe></iframe>
.
In the following, the focus is neither on the formulas nor on a basic introduction to Elm. Instead I present a few for imperatively programming beings*Artificial intelligences are included. like me *I have programmed a lot with Python and C++ in the past. I have almost no experience with JavaScript. As a student, I used JavaScript snippets via copy+paste to exchange button images during mouse hover events. And recently I put together a small Nuxt frontend. unusual aspects. A basic introduction to Elm can be found e.g. in the Elm-Guide. I also had a look at the book Elm in Action*Attention! This is NOT an affiliate link! Attention! purchased. My complete code is on Github.
The last element
The standard sequential data structure in Elm is a list. In contrast to Python lists, there is no random access. So you cannot access the i-th element of the list via someList[i]
. You cannot access the last element of the list neither. Since I needed that for about the whole time, the following little function last
was created.
last : List Float -> Float
last inputs =
Array.fromList inputs
|> Array.get (List.length inputs - 1)
|> Maybe.withDefault - 1
In the first two lines we can see the signature of the function. A list of inputs is expected as an argument. From this a floating point number is calculated. As the imperatively programming being may have noticed, brackets around arguments and a return keyword are missing. Neither is required in Elm. The value of the expression is returned directly without return. In order to return the last element of the list, we convert the list into an Array
that allows random access in Elm. One might wonder if you could save the copy by just using an array all the time. However, other useful functions such as map2
, range
or sum
that I have used elsewhere are not implemented for the type Array
in Elm.
Functions are composed with |>
, i.e.
$$
(f \circ g) (x) = f (g (x))
$$
can be implemented in Elm as g x |> f
.
One of my favorite features in Elm is that partial function application is directly available. For example, if you have defined a function
def g(x, y): [...]
in Python, you can define via
f=partial(g, x=2)
another function f
in y
that equals g
for x=2
. In Elm, an additional higher-order function such as partial
from the Python module functools
is not necessary for partial function application. The partial application of the function g x y = [...]
, i.e., the definition of a function in y
with fixed x = 2
, is simply
g 2
.
Coming back to the function last
, we see that Array.fromList
converts a List
into an Array
and passes the array to the partially applied function Array.get (List.length inputs - 1)
.
The function
Array.get: Int -> Array a -> Maybe a
expects an integer and an array with elements of type a
as arguments and returns the element at the position defined by the integer parameter. Hence, the partially applied function Array.get (List.length inputs - 1)
returns the last element of the array. Strictly speaking, the result is not the last element in the list but a Maybe
, an optional value that may or may not exist. In this post, I do not care about clean error handling, e.g., in the case of an empty list. So I assume the existence of the last element and simply unpack the optional type with the partially applied function Maybe.withDefault -1
. This returns a -1 in the event of an error.
Testing and debugging
From Elm-programmers*Here, too, I include artificial intelligences. But I have not yet met any Elm-programming artificial intelligence. I have read the sentence “If it compiles, it works”. That sounds great, but strictly speaking it goes too far. Of course, the compiler cannot detect logic errors in the program. The easiest way to validate the program’s correctness that I have found is to use the Npm package elm-test
. With the imports
import Expect
import Test exposing (..)
you can write the following test code for the function last
.
testLast : Test
testLast =
describe "last"
[ test "last with 2 elts"
(\_ -> Expect.equal (last [ 0, 4 ]) 4)
, test "last with 1 elts"
(\_ -> Expect.equal (last [ 4 ]) 4)
, test "last with 10 elts"
(\_ -> Expect.equal (last (List.repeat 9 0 ++ [ 4 ])) 4)
, test "last with 0 elts"
(\_ -> Expect.equal (last []) -1)
]
Here, we see another typical feature of functional programming, namely anonymous lambda functions. To understand the notation, consider the example \x -> x * x
. This function calculates the square of its argument x
. The lambda function occurring in the test code
\_ -> Expect.equal (last [ 0, 4 ]) 4)
does not expect any arguments and is a valid second parameter of the function test
.
I have not yet found a sensible way to debug Elm programs interactively. I was able to stop at breakpoints in the generated JavaScript. But I couldn’t make sence of that. If you want to debug with output and without breakpoints, you can use Debug.log
. The function returns its second argument and also logs it into the console. So if you replace line 5 in the above snippet with
(\_ -> Expect.equal (Debug.log "last of [0, 4]" (last [ 0, 4 ])) 4)
the test still works because the second argument of Debug.log
is simply returned. The first argument is of the type String
and serves as a description of the log. When you run the test, you get the output
last of [0, 4]: 4
.
Debug.log
can also be used in program parts that are not test code.
Constants and variables
In order to be able to create a local constant in a function, the keywords let
and in
can be used. As expected, there are no variables in Elm that are not constant. For example, you could rewrite the function last
with a temporary variable as follows, which I have only done here for purely demonstrative purposes.
last : List Float -> Float
last inputs =
let
inputsArr = Array.fromList inputs
in
Array.get (List.length inputs - 1) inputsArr
|> Maybe.withDefault -1
The let
-in
construct can be nested. Within the let
part, in addition to constants, functions can also be defined that are only visible in the in
part.
For-loops und if-expressions
There are no loops in Elm. If possible, you can use functions such as map
, filter
or foldl
. The analogue to foldl
is called reduce
in Python. If the given situation cannot be represented by map
, filter
, foldl
or the like, recursive functions must be used instead of loops. We can see an example in the following snippet.
linearPrices : Float -> Int -> Float -> List Float
linearPrices rate nYears startPrice =
let
recurse : Int -> List Float -> List Float
recurse y prices =
let
newPrices =
linearPricesOfYear rate (last prices)
in
if y < nYears then
newPrices ++ recurse (y + 1) newPrices
else
[]
in
recurse 0 [ startPrice ]
In an intermediate step of the calculator, the linearPrices
function converts an annual interest rate into fictitious exchange rates which are named prices
. Thus, we can use the calculator with slight adjustments with simulated or historical stock prices. A recursive function is defined in the let
-block, which in an imperative language could have been written as a loop. The termination condition is found in line 10, the increment of the counter variables in line 11. The list newPrices
is calculated in the inner let
block. When looking at the termination condition, the imperative programmer could stumble across the question: “What does the function recurse
return?” The answer is that if-else expressions always return a value just like the ternary?: Operator in C++. Further, and as already mentioned, Elm does not know the keyword return
. For instance, if the termination condition is met, the empty list []
is returned.
Types
You can define a so-called record by
type alias Model =
{ rate : Float
, regularPayment : Float
, nYears : Int
}
In Python, a NamedTuple
is perhaps most similar, even if the notation reiminds of a dict
. With type alias
you give an existing type a new name. In contrast, we define the new type Msg
in the following snippet.
type Msg
= ChangedRate String
| ChangedRegPay String
| ChangedYears String
This type is reminiscent of an enum
-type in Python or C++ on the first sight. However, in addition to its value, i.e., either ChangedRate
, ChangedRegPay
, or ChangedYears
, an instance of Msg
can contain something more. In this case, this is a string for each of the values. The following function demonstrates the use of our new and our newly named type with the help of the case
expression.
update : Msg -> Model -> Model
update msg model =
case msg of
ChangedRate newContent ->
{ model
| rate =
String.toFloat newContent
|> Maybe.withDefault 0
}
ChangedRegPay newContent ->
{ model
| regularPayment =
String.toFloat newContent
|> Maybe.withDefault 0
}
ChangedYears newContent ->
{ model
| nYears =
String.toInt newContent
|> Maybe.withDefault 0
}
Let me mention for the imperative programmer that case
returns a value without using a return
keyword. The value of msg
is checked in the case
-expression of the update function. Let us assume w. l. o. g. that msg
has the value ChangedRate
. Then,
case msg of
ChangedRate newContent ->
{ model
| rate =
String.toFloat newContent
|> Maybe.withDefault 0
}
is evaluated. ChangedRate newContent ->
allows access to the string contained in msg
via the name newContent
. The value of newContent
is converted into a floating point number and added to a Model
-record with a new value for the rate
-field. The rest is copied from the model
-argument. This is done through*For exactly this purpose, I often use a self-written function in Python that copies a `NamedTuple` and only replaces the separately passed arguments. I have happily discovered that in Elm this is simply a language feature.
{ model | rate = [...] }
.
We see again that a Maybe
is being unpacked. When converting a string to a float, things can of course go wrong. In the event of an error, a 0 is simply returned here. This can also be seen directly in the calculator above. If you type a letter in one of the fields, the value is set to 0.
Dynamic web applications
I have been talking all the time about individual Elm constructs that are unfamiliar to me as an imperative programmer. Now in this section I would like to briefly explain how dynamic web frontends can be created with Elm. For more precise, better and more extensive explanations, I refer again to the Elm Guide.
Knowledge of HTML is helpful in the following*I am amused that my HTML knowledge, which I acquired as a student, is still relevant 25 years later.. To render the front end, a function view
and a function update
are required. we met the latter already in the last section. The view
function that renders the calculator from above looks as follows.
view : Model -> Html Msg
view model =
div []
[ label [ for "rate" ] [ text "Interest rate in %" ]
, br [] []
, input
[ id "rate"
, value (String.fromFloat model.rate)
, onInput ChangedRate
]
[]
, br [] []
, label [ for "regpay" ] [ text "Payment each month" ]
, br [] []
, input
[ id "regpay"
, value (String.fromFloat model.regularPayment)
, onInput ChangedRegPay
]
[]
, br [] []
, label [ for "years" ] [ text "Years" ]
, br [] []
, input
[ id "years"
, value (String.fromInt model.nYears)
, onInput ChangedYears
]
[]
, br [] []
, div [ style "font-weight" "bold" ]
[ text
(String.fromFloat
((finalBalance (1 + (model.rate / 100))
model.regularPayment
model.nYears
* 100
|> round
|> toFloat
)
/ 100
)
)
]
]
The HTML functions such as div
, label
, input
and br
define the appearance and are imported from the Html
modules*And once again Elm and I agree. The Elm module is called `Html` and not `HTML` although it is an abbreviation. For Camel-Case I prefer this notation because of the better readabilitye, e.g., of `HtmlButton` compared to `HTMLButton`.. They each expect two arguments, a list of attributes and a list of children. In our view
-function the layout is defined and a variable part of the web page is filled with the argument model
. The return value of the view
function is of the type Html Msg
. Msg
is a so-called type variable, comparable to templates in C++ or generics in the programming language, who must not be named*It starts with J and and does not end with avaScript or ulia. Greetings to Nathan.. So Msg
is also a type itself. The content of the Msg
instance is defined by the triggered event handler in one of the input fields. That is, if there is w. l. o. g. a change in the rate
field, onInput ChangedRate
is triggered and the Msg
instance with the value ChangedRate
is sent to the update
function. In addition, the Msg
instance contains the value of the value
attribute. The update
-function adjusts the argument model
according to the change in msg
as seen above in the definition. And the model is then sent back to the view
-function to display the updated status. To render the UI the update
-and view
-functions must be registered in a main
-function.
main =
Browser.sandbox { init = init, update = update, view = view }
An initial Model
is passed as init
parameter. That’s basically it.
This blog post was originally written in German and translated with Google Translate and some light-minded and manual post-processing.