Types

In this chapter we will look at the role played by different Types in Elm. This should be familiar to someone with a functional programming background. The syntax and some details are slightly different in Elm.

In this section we will look at the role played by Types in Elm.

  • Statically typed systems: Languages like C, Java and C# are examples of languages that fall in this category. These languages are characterized by the fact that the types of all variables have to be declared/known at compile time when the type checking is done.

  • Dynamically typed systems: Languages like Python and Ruby are examples of languages that fall in this category. They are characterized by the fact that the types of all variables change depending on what is assigned to them. In other words types are associated with values not variables. These languages typically do most of their type checking at run time.

  • Type Inference: is when the compiler can infer the types of variables and automatically assign them to variables. In case the user has declared the type this information is used to verify the assignment. Type checking like static languages is done at compile time. This approach tries to keep the benefits of static typing while allowing the programmer to not "have to" declare the types all the time.

The subject of type systems is vast and we cannot do justice to the subject here, so I have only highlighted a few interesting features. You can begin a long journey of understanding and analyzing Type Systems here. It is tempting to think that we have the best situation when we languages that support static typing where possible and dynamic typing where needed.

Elm, as we will see is a statically typed language with support for type inference.

Here we can see a list of

Basic Types

Let us create an instance of each of the Basic types:

import Graphics.Element exposing (..)

aInt : Int
aInt = 10

aFloat : Float
aFloat = 2.0

aString : String
aString= "Hi"

aChar : Char
aChar = 'c'

aBool : Bool
aBool = True

main = flow down [
           print "The value of aInt is : " (aInt)
          ,print "The value of aFloat is : " (aFloat)
          ,print "The value of aString is : " (aString)
          ,print "The value of aChar is : " (aFloat)
          ,print "The value of aBool is : " (aFloat)
         ]

--a helper function to make display easier
print message value = show (message ++ (toString value))

You can just enter this code in the online editor/runner.

Variables and Types

A variable consists of three things:

  • Name: Something you assign, it has to start with a lower case letter. In the above example you have variable names aFloat, aString etc.
  • Type: This is something that you assign to a variable using the syntax name : Type, Elm will infer this for you if you do not explicitly assign.
  • Value: This is the value stored in the variable and it has to be compatible with the Type you assigned to the variable. If you are not assigning the type, Elm uses the value to infer the type of the variable.

Type Aliases

Type aliases are not a mechanism by which you can give more relevant type names to built in types. Here are a couple of examples:

import Graphics.Element exposing (..)

type alias Name = String
type alias Id = Int

aName : names
aName = "Jim"

aId : Id
aId = 10001

main = flow down [
            print "The value of aName is : " (aName)
           ,print "The value of aId is : " (aId)
          ]
--a helper function to make display easier
print message value = show (message ++ (toString value))

Records

Even though functional programming is all about programming with functions, we still need to work/operate on data. This is where records come in.

For example let us consider we want to work with data for an Employee which we take to contain anid : Int , firstName : String, lastName : String. We can use a record to represent this data. While it takes a little time to design good records to represent your data, they will make your programs easier to understand and maintain.


import Graphics.Element exposing (..)

-- creating a record
aEmployee = {id = 101 , firstName = "Michael", lastName = "Jordan"}

-- accessing the fields
fName = aEmployee.firstName
lName = aEmployee.lastName

-- field accessors
fName1 =
  List.map .firstName[
      aEmployee
      ,{id = 101, firstName = "Michael", lastName = "Jordan"}
    ]   

-- update fields  of a Record (Notice we did not mutate the id so it is copied over)
aNewEmployee =
  {aEmployee | firstName = "Magic", lastName = "Johnson" }  

type alias Employee =
  {id : Int, firstName : String, lastName : String}

anotherEmployee : Employee
anotherEmployee =
  {id=103, firstName = "Larry", lastName = "Bird" }

-- pattern matching using record accessors
getName:{ a | firstName : String, lastName : String } -> String
getName {firstName, lastName} =
  firstName ++ lastName

main = flow down [
             print "The value of aEmployee is : " (aEmployee)
            ,print "The value of aNewEmployee is : " (aNewEmployee)
            ,print "The value of anotherEmployee is : " (anotherEmployee)
            ,print "The value of anotherEmployee is : " (getName aEmployee)
          ]

--a helper function to make display easier
print message value = show (message ++ (toString value))

Records are a lightweight data structure similar to a tuple in that it can hold a fixed number of different types. Unlike tuples the elements of a record have names and accessors that allow us to work with the data stored in a record. You can work without type aliases for a record, but if you are going to create multiple instances of the record, then it is more compact and less error prone to create the type alias.

It is important to keep in mind some of the distinctions between Objects and Records, there is a great description of this here, along with more information on records.

Enumerations

Often we need to describe fixed sets of values in our programs. For example we can describe the state of a program as {NotStarted, Running, Completed}. Of course we could represent this as a set of integers, but Enumerations are a much nicer way to do this. We then use the Enumerations typically as switches to control the logic we use to handle these different cases.

import Graphics.Element exposing (..)

type Status = NotStarted|Running|Completed|Crashed

log : Status -> String
log status =
 case status of
   NotStarted -> "The program is not started"
   Running -> "The program is Running"
   Completed -> "The program is Completed"
   Crashed -> "The program has Crashed"

main = flow down [
             print "Program Status: " (log NotStarted)
            ,print "Program Status: " (log Running)
            ,print "Program Status: " (log Completed)
            ,print "Program Status: " (log Crashed)
          ]
--a helper function to make display easier
print message value = show (message ++ (value))

In this case we have just implemented a simple function that converts the different Status cases to strings. But you could write similar functions to trigger different functions based on the Status enumeration.

import Graphics.Element exposing (..)

type Direction = Left|Right|Up|Down

type alias Point = { x : Float, y : Float}

movePoint : Point -> Direction -> Point
movePoint aPoint aDirection =
     case aDirection of
      Left -> {aPoint | x = aPoint.x - 1}
      Right -> {aPoint | x = aPoint.x + 1}
      Up -> {aPoint | y = aPoint.y + 1}
      Down -> {aPoint | y = aPoint.y - 1}

p : Point
p = {x=0, y=0}   

main = flow down [
             print "The starting point p : " (movePoint p Up)
            ,print "P moved Up gives : " (movePoint p Up)
            ,print "p moved Down gives" (movePoint p Down)
          ]
--a helper function to make display easier
print message value = show (message ++ (toString value))

Algebraic Data Types

An algebraic data type (ADT) allows us to create a composite type that consists of other types. (A record is also a composite type which requires all the contained sub types that go into its definition to be present as specified.)

Union Types - OR Types:

Let us look at an example of an ADT (Union Type) arising in a in a situation where we are modeling two types of geometric objects: Circles and Squares. Then we could write a program to manipulate them as follows:

import Graphics.Element exposing (..)
import Graphics.Collage exposing (..)
import Color exposing (..)

type alias Radius = Float
type alias Side = Float
type Shape = Circle  Radius | Square Side

drawShape : Shape -> Graphics.Collage.Shape
drawShape aShape =
    case aShape of
      Circle radius-> Graphics.Collage.circle radius
      Square side -> Graphics.Collage.square side

areaShape : Shape -> Float
areaShape aShape =
    case aShape of
      Circle radius-> 3.14 * radius*radius
      Square side -> side * side

aCircle = Circle 50
aSquare = Square 100
main =
 collage 300 300
  [filled red (drawShape (aSquare))
  ,filled blue (drawShape (aCircle))
  ,print "The area of the circle is : " (toString(areaShape (aCircle)))
  --,print "The area of the square is : " (toString(areaShape (aSquare)))
  ]

--a helper function to make display easier
print message value = move (0, -100) <|toForm <| show (message ++ (toString value)++ "pixels")

Notice how using pattern matching we are able to route the flow of the program to the appropriate section of code based on the type.

Generics or Parametrized Types

Now we come to an extremely powerful concept; can we write code that will work for all types. This is especially useful when we are designing data structures that store different types. We see in this section that types can take parameters that are other types. And this will give us great flexibility and we can write more "generic" code.

Creating an ADT:

So let us look at a simple data structure, suppose we have a situation where we we want to have a type that can have either an Integer or be empty.

Here is a simple implementation using ADTs:

 import Graphics.Element exposing (..)
 import Graphics.Collage exposing (..)
 import Color exposing (..)

 type OptionalInt = Nothing |OptionalInt Int

 extract : OptionalInt -> String
 extract aOptionalInt =
   case aOptionalInt of
     OptionalInt x -> toString(x)
     Nothing -> ""

 aOptionalInt = OptionalInt 3

 main =
  flow down
   [print "The value stored in OptionalInt is : " (extract aOptionalInt)
   --,print "The area of the square is : " (toString(areaShape (aSquare)))
   ]

 --a helper function to make display easier
 print message value = show (message ++ (toString value))
 {% endhighlight %}

So that is great, we are about to celebrate when we realize that we need to have OptionalFloat as well!

__Creating a Parametrized ADT :__

The best way to create containers that can store multiple types is to use a parametrized ADT. Here is an example where we  modify the OptionalInt type to a parametrized ADT so that it can store any type!

{% highlight Haskell %}
import Graphics.Element exposing (..)
import Graphics.Collage exposing (..)
import Color exposing (..)

type OptionalValue a = Nothing |OptionalValue a

extract : OptionalValue a -> String
extract aOptionalValue =
  case aOptionalValue of
    OptionalValue x -> toString(x)
    Nothing -> ""

intOptionalValue : OptionalValue Int
intOptionalValue = OptionalValue 3

floatOptionalValue : OptionalValue Float
floatOptionalValue = OptionalValue 5.0

main =
 flow down
  [print "The value stored in OptionalValue is : " (extract intOptionalValue)
   ,print "The value stored in OptionalValue is : " (extract floatOptionalValue)
  ]

--a helper function to make display easier
print message value = show (message ++ (toString value))

As the type declaration shows type OptionalValue a = Nothing |OptionalValue a OptionalValue is a parametrized type that takes a type parameter a as part of its definition.

Then when we create an instances of OptionalValue like:

intOptionalValue : OptionalValue Int
intOptionalValue = OptionalValue 3

floatOptionalValue : OptionalValue Float
floatOptionalValue = OptionalValue 5.0

we specify the type parameter that we want to use.

You will find that using parametrized types an extremely simple, expressive and powerful programming technique.

In Elm there is a built in type called Maybe that is the same as the Optional type that we just created for illustration purposes.

Recursive Types

And now we come to the question can a type refer to itself in its definition? You might ask why do I want to do that? Here is a simple example of a binary tree from the elm documentation:

import Graphics.Element exposing (..)
import Text

type Tree a
    = Empty
    | Node a (Tree a) (Tree a)

empty : Tree a
empty =
    Empty

singleton : a -> Tree a
singleton v =
    Node v Empty Empty

insert : comparable -> Tree comparable -> Tree comparable
insert x tree =
    case tree of
      Empty ->
          singleton x

      Node y left right ->
          if x > y then
              Node y left (insert x right)

          else if x < y then
              Node y (insert x left) right

          else
              tree

fromList : List comparable -> Tree comparable
fromList xs =
    List.foldl insert empty xs

depth : Tree a -> Int
depth tree =
    case tree of
      Empty -> 0
      Node v left right ->
          1 + max (depth left) (depth right)

map : (a -> b) -> Tree a -> Tree b
map f tree =
    case tree of
      Empty -> Empty
      Node v left right ->
          Node (f v) (map f left) (map f right)

t1 = fromList [1,2,3]
t2 = fromList [2,1,3]

main : Element
main =
    flow down
        [ display "depth" depth t1
        , display "depth" depth t2
        , display "map ((+)1)" (map ((+)1)) t2
        ]

display : String -> (Tree a -> b) -> Tree a -> Element
display name f value =
    name ++ " (" ++ toString value ++ ") &rArr;\n    " ++ toString (f value) ++ "\n "
        |> Text.fromString
        |> Text.monospace
        |> leftAligned

Type Classes - number, appendable, comparable etc.

I will not get into a discussion on Typeclasses as the creators of Elm wanted to suppress the complexity of the more advanced usage of types. (You will have to refer to my Haskell notes to see a description of typeclasses.) Anyway, number (is essentially a built-in type class in Elm, there are a few more like this (e.g appendable)) and represents a type that supports addition, subtraction, etc. I did not manage to find a lot of documentation on typeclasses in Elm so these comments are based on my experimentation and may be inaccurate.

import Graphics.Element exposing (..)

aInt :Int
aInt =3

aFloat : Float
aFloat = 3.0

triple : number -> number
triple number =
  3*number

main = flow down [
            print "The value of triple aInt is : " (triple aInt)
           ,print "The value of triple aFloat is : " (triple aFloat)
          ]

--a helper function to make display easier
print message value = show (message ++ (toString value))

So what is see above is a simple use of the number type. The function triple is declare to have type triple : number -> number. So that means this function will accept any type that supports operations like +, - etc. (or more precisely is an instance of the number typeclass.)

Okay, that is about all the detail we will go into here on typeclasses but at least you get a sense of how to use it, and a flavor of what it means.

Collections:

Elm has built in support for some essential collection data types:

  • Lists : are a collection of values of the same type, that can have different/changing length.
  • Tuples : are kind of "dual" to lists in that they are collections that have fixed length, but can have varying types.
  • Dictionaries are an extensible collection that stores pairs of keys and values.
  • Arrays

A simple example of the usage of the Elm dictionary:

import Graphics.Element exposing (..)
import Dict exposing (Dict)

a : Dict String Int
a = Dict.insert "one" 1 Dict.empty

b = Dict.insert "two" 2 a

c : Dict String Int
c= Dict.update "two" triple b

-- use this function to update a value if found
triple : Maybe Int -> Maybe Int
triple i =
 case i of
   Nothing -> Just 0
   Just i  -> Just (3 * i)

main = flow down [
          print "a is: " a
         ,print "b is: " b
         ,print "c is: " c
        ]

--a helper function to make display easier
print message value = show (message ++ (toString value))

results matching ""

    No results matching ""