Functional Reactive Programing with Elm: Part II - Elm Basics
In this post we will look at the basics of programming in Elm. We will also encounter some of the essential concepts of Functional programming.
You can just enter this code in the online editor/runner.
Let us see if we can understand how this code works.
First we define the function double
that takes a number and doubles it. The elm compiler infers this “function signature” and this is how it is expressed in Elm syntax double : number -> number
. You can read this as double is a function that returns a number and takes a number as input
.
It is always good practice to figure out the signature of the functions that we are creating and declare it. This way the compiler will verify it for you and let you know if the function that you have created has a bug in it and does not match the signature.
Okay time to make some functions of your own.
Exercises
-
Create the functions
subtract
,divide
,multiply
just like we did theadd
function. -
Create a function to convert from kilograms to pounds.
-
Slightly more challenging create a function to check if a string is a palindrome. (Hint: there is a function called
reverse
that you can use. In case you are stuck you can see the solution below.)
Recursive Functions
Recursive functions are simply functions that refer to/call themselves in their definition. Recursion is a powerful technique to solve problems in an intuitive and compact manner. The common example used to illustrate this is the factorial function: . (Okay for those of you interested in mathematics and typesetting to see the previous expression, we have to get $ \LaTeX $ working! Latex, Javascript, Jekyll and markdown can all work with each other is something incredible. The fact that I was about to get it up an running in a few minutes is amazing!) As you can see the definition of the function refers to itself. This is natural to do in mathematics, can we do the same in an Elm program? The answer is yes. Let us see how this looks:
The recursion pattern has two ingredients:
- The termination condition: Need to handle all conditions that the recursion has to terminate for. For the factorial function it is when n gets to 0 we terminate the recursion. For those using recursion this is often the source of bugs, which leads to infinite recursion and program crashes!
- The recursive step: For all values of n other than the terminal one we describe how to compute the next value.
The second example that I have included generates the Fibonacci numbers. These numbers we noticed much earlier by Hemachandra.
Fields medallist Manjul Bhargava has been referring to the Hemanchandra numbers and how he was inspired by ancient indian mathematicians. So we called the function hemachandra
in this example.
While recursion is a good technique to use to solve problems,
often in programs we just end up combining library functions to write our code. So we could write the factorial function as
factorial n = product [1..n]
.
Exercises
- Define a recursive function
power
such thatpower x y
raisesx
to they
power.
Pattern Matching
The technique of using pattern matching to to implement logic in your code is a very standard technique in functional programming. In some sense it is a very natural way to have different code to handle different patterns. There are quite a few different things that you can use as patterns:
- Values
- Types
- Guards/Expression
- special patterns related to data structures (like [],(x::xs) for lists)
Right now let us use pattern matching on values.
As you can see from the code above there are three patterns that are tested against the current value of n
either n
is 0, 1 or any other value.
Pattern Matching on Lists
Combining recursion with pattern matching on lists gives us the technique to write many interesting functions. Here we have shown three functions (length
, reverse
, head
) for you to review.
Exercises
- Implement the function
take n ys
that returns the first $n$ elements of the list of $ys$.
In case you want to just look at the solution here it is.
Higher Order Functions - Maps, Filters and Folds
Higher order functions is a fancy term for functions that take other functions as parameter. Having said that there is an interesting way to look at this as a paradigm shift in the way we think about functions. The normal way we use functions is to say to the function here is some data (parameters) can you please run yourself on my data. On the other hand with higher order functions you are saying here is some code can you please run this code for me in your context.
Exercises
- Implement the cartesian product of two lists. Given two lists
l1= [1,2,3] l2=[1,2,3,4]
output the listl3= [(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4),(3,1),(3,2),(3,3),(3,4)]
(This exercise was suggested to me by my friend, and it really illustrates how difficult it is to code until you have mastered the functional programming idioms.)
You should look up the definition of concatMap and other functions defined in the List module.
Anonymous Functions
The term Lambda function or Lambda expression comes from Lambda Calculus. Introduced by Alonzo Church in the 1930’s as a part of a study of the foundations of mathematics. Just like the [Turing machine is a fundamental model for computation, Lambda Calculus provides a equivalent model for computation. This little comment is a glimpse into a very interesting world of formal analysis of mathematics and computation that I encourage you to follow in your leisure.
Most of the functions that we have encountered so far have names. But are the names really important? Actually, names are important only if we want to reuse the functions in many places. Then, referring to them by their name is quite handy.
We use Lambda functions most often when: * We would like to just create a function “right where we need it” * We only plan to use the function once. * The function is fairly simple * We want to return a function from a function.
Usually we do this to pass/receive these functions to other functions that need them.
Here are a couple of examples of Lambda Functions:
Exercises
-
Define an anonymous function and use it to double all the elements of a list.
-
By combining some of the techniques described about implement the quicksort algorithm to sort a list.
Function Composition and Pipes
In Elm the operator for function composition is >>
and <<
:
In addition there is also the |>
operator that allows us to pass the output of one expression as the last argument of the next function. (Exercise: check this.)
Closures
The main idea behind currying is that functions that have n
parameters can be reduced to a functions that have a single parameter.
In other words we are saying that
f : a -> b -> c
is the curried form of g : (a,b) -> c
.
Of course this means that we are saying that the two function f and g are the same or ` f x y = g (x,y)`.
Let us see what happens when we perform partial application of functions. In other words if a function expects 2 arguments (for example) then we can create a new function with 1 argument by partial application.
import Graphics.Element exposing (..)
add : Int -> Int -> Int
add x y =
x + y
-- create increment by partiial application (add 1)
increment : Int -> Int
increment = add 1
main = flow down [
print "add 2 3 : " (add 2 3)
,print "increment 3 gives : " (increment 3)
]
--a helper function to make display easier
print message value = show (message ++ (toString value))
In the example above we were able to create the function
increment : Int -> Int
by partial application from the function
add : Int -> Int -> Int
. As you can see the number of parameters in the function increment is less than that of the function add. note: Elm does not give us an error saying that it expected two parameters, it returns a new function with one less argument. This behavior needs getting used to as sometimes you will find that in your code if you miss a paramter the error message will be about problems caused by this new function that you have unintentionally created.
This example give us the heuristics for currying. We can read the signature add : Int -> Int -> Int
either as saying that add is a function that takes two Integers and returns an Integer or add is a function that given an Int it returns a function from (Int -> Int). And these two representations are equivalent. We just used partial application to see what this looks like.
For the most part Currying is formal and does not impact the new developer. I would say understanding partial application is quite useful in many situations.
Sections
Another related idea is that we can also use partial applications on infix operators. Here are some examples of sections:
import Graphics.Element exposing (..)
increment : Int -> Int
increment = ((+) 1)
twoToPower : Int -> Int
twoToPower = ((^)2)
square : Int -> Int
square = (flip (^)2)
main = flow down [
print "2 to the power 3 : " (twoToPower 3)
,print "increment 3 gives : " (increment 3)
,print "square of 3 is : " (square 3)
]
--a helper function to make display easier
print message value = show (message ++ (toString value))
Closures
Now we come to a very interesting concept that we must understand if we are going to pass functions to other functions.
Consider this example:
import Graphics.Element exposing (..)
createIncrementer : Int -> Int -> Int
createIncrementer n =
let incrementValue = n
in ((+) incrementValue)
incrementByOne : Int -> Int
incrementByOne = createIncrementer 1
incrementByTwo : Int -> Int
incrementByTwo = createIncrementer 2
main = flow down [
print "incrementByOne 3 : " (incrementByOne 3)
,print "incrementByTwo gives : " (incrementByTwo 3)
]
--a helper function to make display easier
print message value = show (message ++ (toString value))
In this example the function createIncrementer
returns a function. But that function depends on the local variable incrementValue
that is defined within createIncrementer
.
What is intesting is that the returned functions, incrementByOne
and incrementByTwo
remember/capture the value of this local variable at the time they were created. So they have captured some of the state of the function that they were created in. This is called “closing over the local variable” and more generally this behavior is called a closure.