title |
---|
Fibonacci Bars |
The FibonacciBars.elm example program displays a pile of rectangles. Their widths correspond to the values of the first Fibonacci numbers. You can see the bars here.
Before explaining how that program is written, let’s first make a
small detour, and introduce the REPL (Read Eval Print Loop) tool, that
Elm — like many other functional languages — provides. The elm-repl
command starts it, like so:
$ elm-repl
Elm REPL 0.4 <https://github.com/elm-lang/elm-repl#elm-repl>
Type :help for help, :exit to exit
>
The REPL prints the initial message and the prompt >
character. We
can now enter Elm statements and evaluate Elm expressions. Let’s first
add two numbers:
> 1.0 + 6.5
7.5 : Float
We added two floating-point numbers. Elm printed the result followed
by its type, Float
, which denotes floating point numbers. The value
and the type are separated with the :
character. We will now round a
floating point number by calling the round
function. In addition to
that, we will assign a name to the result:
> x = round 7.5
8 : Int
In order to call a function, we use its name, followed by a whitespace
character (or more then one), followed by the function
parameter. Notice, that not only the value has changed (it has been
rounded), but its type as well. It is now Int
, which represents
integer numbers. By preceding the function call expression with a name
and the equals sign, we have assigned the name to the result
value.
We can use the REPL to find out the signature of the round
function
by entering its name alone.
> round
<function: round> : Float -> Int
As you could expect, the signature of the round
function says that
the function takes a floating point number and returns an
integer. Let’s now add something to x
.
> x + 1.0
Error in repl-temp-000.elm:
Type mismatch between the following types on line 3, column 3 to 10:
Int
Float
It is related to the following expression:
x + 1.0
What is that? We got a compilation error. We tried to add x
of type
Int
to 1.0
, which is a floating-point number of type Float
. Elm
does not allow adding an Int
value to a Float
value. We need to
turn x
into a Float
first. We can do it using the toFloat
function.
> toFloat x
8 : Float
> toFloat x + 1.0
9 : Float
Notice, that the function application binds more than the addition. In other words, the above expression is equivalent to the following one:
> (toFloat x) + 1.0
9 : Float
Let’s give another value a name:
> y = 4
4 : number
Wait a moment, what is its type? Didn’t I write that type names must
always start with capital letters? Actually, the type of y
is not
yet decided by the Elm compiler. It could be an integer value, but it
could also be a floating-point number. Elm does not want to decide too
early. We can add y
to both an integer value and a floating-point
value:
> x + y
12 : Int
> 1.0 + y
5 : Float
In both cases we got results of different types. The first result is
of type Int
, since we are adding a number
to an Int
value. Elm
decides to treat the number as an Int
, and adds it to the other
Int
value. In the other case, we are adding a number
to a Float
value. Elm decides to treat the number as a Float
value, and adds
it to the other Float
value. It is important to understand how Elm
treats numbers. Some Elm standard functions require parameters of type
Int
, while other functions require parameters of type Float
. If
you provide an argument of a wrong type, you will get a compilation
error. In that case you may need to use a conversion function, like
round
or toFloat
.
Besides, the +
, -
and *
operators for addition, substracting and
multiplication, Elm provides two operators for division: /
and
//
. One is operating on Float
values and the other one on Int
values. Let’s use the REPL to show their signatures. However, since
they are operators, and not functions with alphanumeric names, we need
to enclose them in parenthesis.
> (/)
<function> : Float -> Float -> Float
> (//)
<function> : Int -> Int -> Int
There is also the %
operator, for taking a modulo of two numbers.
> (%)
<function> : Int -> Int -> Int
Here are examples of using those operators:
> 10 / 3
3.3333333333333335 : Float
> 10 // 3
3 : Int
> 10 % 3
1 : Int
To exit the REPL, use the :exit
command.
> :exit
Let’s now go back to our program displaying colorful bars. The program
consists of two files. The FibonacciBars.elm
file defines the main
function. But let’s first a look at the
Fibonacci.elm file, which defines the Fibonacci
module. Here is its content:
% Fibonacci.elm module Fibonacci where
import List ((::), head, map2, reverse, tail)
fibonacci : Int -> List Int
fibonacci n =
let fibonacci' n acc =
if n <= 2
then acc
else fibonacci' (n-1) ((head acc + (tail >> head) acc) :: acc)
in
fibonacci' n [1,1] |> reverse
fibonacciWithIndexes : Int -> List (Int,Int)
fibonacciWithIndexes n = map2 (,) [0..n] (fibonacci n)
It begins with a module declaration and an import statement. Since
::
is an operator, it needs to be enclosed in parenthesis in the
import statement.
Next comes the fibonacci
function for calculating the first n
Fibonacci numbers. Its type declaration specifies, that it takes one
argument of type Int
and returns a list of integers List Int
.
A list literal is a sequence of the list elements, enclosed in square brackets, and separated by commas:
> ["a","b","c","d"]
["a","b","c","d"] : List String
The fibonacci
function uses the auxiliary fibonacci'
function
(yes, the aphostrophe character '
can be used in identifiers; by
convention identifiers ending with '
are somehow related to similar
identifiers without the '
character). The auxiliary function is
defined within the let
expression, which has the following
structure:
let <definitions> in <expression>
The result of the let
expression is the value of the expression
defined after the in
keyword, but that expression may use the
definitions placed between the let
and in
keywords. The
fibonacci'
function is recursive — it calls itself. It has two
parameters: n
and acc
. It is initially called with the parameter
n
equal to the number of Fibonacci numbers to be generated, and the
[1,1]
list, which represents the first two numbers.
The body consists of an if/then/else
expression, which is a
conditional expression with the following structure:
if <condition> then <result1> else <result2>
The expression calculates its value in one of two distinct ways,
depending on the value of the condition placed between the if
and
then
keywords. The condition must evaluate to a boolean value, that
is a value of type Bool
, which has two possibles values: True
or
False
.
If the conditional expression evaluates to True
, the result of the
whole if
expression is the result of evaluating the expression after
the then
keyword. Otherwise, it is the result of evaluating the
expression after the else
keyword. In our case, when n
is less
then or equal to 2
, the fibonacci'
function returns the value of
the accumulator acc
. Otherwise, it calls itself with the new values
of n
and acc
calculated by the expression:
(head acc + (tail >> head) acc) :: acc
The head
function returns the first element of a list. The tail
function returns a list without its first element.
> import List (..)
> head [1,2,3,4]
1 : number
> tail [1,2,3,4]
[2,3,4] : List number
The composition of tail
and head
returns the second element from
the list. Two functions can be composed in Elm using the >>
or <<
operators. The f (g x)
expression is equivalent to both (g >> f) x
and (f << g) x
— both operators are equivalent, except for the order
of arguments.
> head (tail [1,2,3,4])
2 : number
> (head << tail) [1,2,3,4]
2 : number
> (tail >> head) [1,2,3,4]
2 : number
The next fibonacci number is calculated as the sum of the first and
the second element of the current list of results (the accumulator
acc
) and is stored as the value next
.
The new accumulator is calculated by prepending its current value with
the next value. The ::
operator yields a new list with its left
operand (an element) prepended to its right operand (a list).
> 7 :: [1,2,3,4]
[7,1,2,3,4] : List number
The new n
value for the recursive call to fibonacci'
is equal to
the old n
decremeted by 1.
The result of calling the fibonacci'
function is reversed using the
reverse
function, since fibonacci'
returns the list of numbers in
reversed order.
> reverse [1,2,3,4]
[4,3,2,1] : List number
We can test our fibonacci
function in the REPL. First, we import
functions from the Fibonacci
module.
> import Fibonacci (..)
We can now call the fibonacci
function.
> fibonacci 10
[1,1,2,3,5,8,13,21,34,55] : List Int
The bars displayed by our program have the width proportional to the
first ten Fibonacci numbers. But their colors are calculated
differently. They are selected from a list of seven colors based on
the index (position) of each number. We will need a function that
returns a list of pairs containing the Fibonacci numbers paired with
their positions. The fibonacciWithIndexes
function calculates such
pairs.
> fibonacciWithIndexes 10
[(0,1),(1,1),(2,2),(3,3),(4,5),(5,8),(6,13),(7,21),(8,34),(9,55)] : [(Int, Int)]
A pair of values is enclosed in parenthesis and separated by a comma.
> (1,"a")
(1,"a") : (number, String)
Unlike a list, both values of a pair may have different types. A pair is a 2-element tuple. We can also create tuples containing more elements.
> (1,"a",4.0)
(1,"a",4) : (number, String, Float)
> (1,"a",4.0,6)
(1,"a",4,6) : (number, String, Float, number)
There is another way we can use to create tuples:
> (,) 1 "a"
(1,"a") : ( number, String )
> (,,) 1 "a" 4.0
(1,"a",4) : ( number, String, Float )
> (,,,) 1 "a" 4.0 6
(1,"a",4,6) : ( number, String, Float, number )
(,)
, (,,)
and (,,,)
are actually functions returning tuples.
> (,)
<function> : a -> b -> ( a, b )
> (,,)
<function> : a -> b -> c -> ( a, b, c )
> (,,,)
<function> : a -> b -> c -> d -> ( a, b, c, d )
Those functions are generic, or polimorphic — the letters a
,
b
, c
and d
denote type parameters.
The list of consecutive numbers from 1 to 20 is generated by the
[1..20]
expression.
> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] : List number
The map2
function takes a two-argument function and two lists, and
returns a list of values calculated by applying the function to
consecutive elements of both lists, skipping the excessive elements of
the longer list.
> map2 (+) [9,8,7,6,5] [4,4,7,7]
[13,12,14,13] : List number
> map2 (,) [9,8,7,6,5] [4,4,7,7]
[(9,4),(8,4),(7,7),(6,7)] : List ( number, number )
Let’s now look at the contents of the FibonacciBars.elm file.
% FibonacciBars.elm module FibonacciBars where
import Color (blue, brown, green, orange, purple, red, yellow)
import Fibonacci (fibonacci, fibonacciWithIndexes)
import Graphics.Collage (collage, filled, rect)
import Graphics.Element (down, flow, right)
import List (drop, head, length, map)
import Text (asText)
color n =
let colors = [ red, orange, yellow, green, blue, purple, brown ]
in
drop (n % (length colors)) colors |> head
bar (index, n) =
flow right [
collage (n*20) 20 [ filled (color index) (rect (toFloat n * 20) 20) ],
asText n
]
main = flow down <| map bar (fibonacciWithIndexes 10)
Again, we start with the module declaration. We then proceed by
importing both functions from the Fibonacci
module and several
members from the standard library modules. Next, we have three
function declarations. Let’s analyze them one by one.
The color
function returns one of seven colors based on the value of
its parameter. The seven colors are defined in the let
expression,
in a list. The color names used are all available in the Color
module. The function calculates its result by dropping (using the
drop
function) a number of elements from the list of colors, and
then taking the first element of the remaining list. The drop
function
takes two arguments: the number of elements to be dropped and a list.
> drop 2 [1,2,3,4,5]
[3,4,5] : List number
The number of elements to be dropped is calculated by taking a modulo
of n
and the length of the list of colors.
The bar
function produces a colored rectangle together with a number
corresponding to its length. Since it is our first function which
draws something, let’s analyze it in detail, piece by piece.
The rect (toFloat n * 20) 20
expression uses the rect
function to
produce a rectangular “shape”. The rect
function is defined in the
Graphics.Collage
module. It takes two Float
values representing
the width and height of the rectangle, and returns a value of type
Shape
:
rect : Float -> Float -> Shape
There are other functions creating shapes in the Graphics.Collage
module: oval
, circle
, square
, ngon
, polygon
. The shape
created by the rect
function is used as an argument to the filled
function in the following expression:
filled (color index) (rect (toFloat n * 20) 20)
The filled
function takes a color and a shape, and creates a value
of type Form
representing the filled (colored) rectangle:
filled : Color -> Shape -> Form
Other functions creating forms from shapes are available as well:
textured
, gradient
, outlined
. The Color
argument is calculated
by the color
function, described above.
The next step is transforming a Form
into an Element
. This is the
job of the collage
function:
collage : Int -> Int -> List Form -> Element
The collage
function takes two integer values and a list of forms
and turns them into an Element
. The two integer values represent the
element width and height. The element created by collage
in the
bar
function creates an element which has the same size as the
rectangular form that it contains:
collage (n*20) 20 [filled (color index) (rect (toFloat n * 20) 20)]
The flow
function, defined in the Graphics.Element
module, creates
an Element
from a list of elements:
flow : Direction -> List Element -> Element
The Direction
parameter represents the direction of how the elements
from the list are placed in relation to each other. The following
functions return various Direction
values: left
, right
, up
,
down
, inward
, outward
.
In the bar
function, flow
is used to create an element consisting
of the rectangular bar and a numeric value to its right (thus the
right
function used for the Direction
argument). The numeric value
is turned into an Element
using the asText
function. That function
can be used to turn any value into a value of type Element
(not into
a value of type Text
!):
asText : a -> Element
A small letter, like a
, in the type signature denotes a type. Thus the
signature says, that asText
turns any type a
(whatever it is) into
an Element
.
The main
function uses the flow
function as well, this time
putting the elements downwards. The second argument of the flow
function is preceded by the <|
operator. That operator is basically
the function application opeator: f <| a
is equivalend to f a
,
except that <|
has low binding priority. Therefore, it can be used
when we do not want to enclose some expression in parenthesis. Let’s
explain it on an example. The following two lines of code are
equivalent:
flow down (map bar (fibonacciWithIndexes 10))
flow down <| map bar (fibonacciWithIndexes 10)
They both represent a flow
function call with two arguments. In the
first line, the second argument is enclosed in parentheses. In the
second line, since the <|
operator has a low binding priority, the
expression to its right is calculated first and its result is used as
the argument to the flow down
function (which represents a partial
application of the flow
function).
The expression used as the second argument of the flow
function
uses the map
function, which has the following signature:
> map
<function: map> : (a -> b) -> List a -> List b
Notice, that we have used the REPL to verify the signature. We just entered the name of the function, and REPL responded with its signature.
The map
function has two arguments. The first one is a one-argument
function — a function that takes values of some type a
and returns
values of some type b
. The second argument is a list of elements of
type a
. The map
function returns a list of elements of type b
by
applying the function to each element of the input list. Here are
three examples of using the map
function with various arguments.
> map round [1.2,4.5,7.8,98]
[1,5,8,98] : List Int
> map fibonacci [3,4]
[[1,1,2],[1,1,2,3]] : List (List Int)
> map (\\x -> x+1) [1,2,3,4]
[2,3,4,5] : List number
In the second example we used the fibonacci
function, obtaining a
list of lists.
The third example introduces a new concept — an anonymous function. It
is a function without a name. It starts with the \\
character, which
is followed by the name (or names) of function arguments. The
arguments are followed by the ->
arrow and the function body. In the
example, the anonymous function has one parameter x
, and returns the
input value incremented by one.
In the present chapter, we have been able to perform some mathematical calculations as well as show some graphics. However, our program is static. In the next chapter we will show how Elm lets write dynamic programs by means of signals.