The wonders of technology

Inspired by a colleague, I've taken up Haskell again. To do so, I've been re-reading Learn You a Haskell. I have the ebook on my iPad, along with an app called Raskell. Raskell is a "Haskell development environment for your iPad"; naturally, it's a little quirky due to the form factor but it works well for what it does. It means that I can do this

Raskell

I can also switch quickly back and forth (or just bring up a small preview of the page I'm in the book to quickly verify something):

Switching between book and code

As can be seen in the screenshot, I'm quite enamoured of Literate Haskell. One of my favourite things about Go is godoc, and it appears that Literate Haskell (in conjunction with pandoc) will let me produce quality code illustrations will less effort, as well as improve documentation in general.

The nice thing about Literate Haskell is that the rest of this post is an example of a Literate Haskell program that was converted to markdown using pandoc 08typeclass.lhs -o 08typeclass.md.

Making Our Own Types and Typeclasses

Types are defined using the data keyword like so:

data Bool' = False | True

More complex types may also be defined:

 -- Circle x y r
 -- Rectangle x1 y1 x2 y2
 data Shape = Circle Float Float Float |
              Rectangle Float Float Float Float
              deriving Show

The deriving Show declaration allows the types to be displayed, eg. given

niceRectangle :: Shape
niceRectangle = Rectangle 0 0 3 4

Without show, calling niceRectangle from the REPL will trigger an error:

ghci> niceRectangle
ERROR - Cannot find "show" function for:
*** Expression : niceRectangle
*** Of type    : Shape

With show, calling this from the REPL will show something to the effect of

ghci> niceRectangle
Rectangle 0.0 0.0 3.0 4.0

The value constructors (Circle and Rectangle) may be used in pattern matching.

surface :: Shape -> Float
surface (Circle _ _ r) = pi * r^2
surface (Rectangle x1 y1 x2 y2) = (abs $ x1 - x2) + (abs $ y1 - y2)

It's important to understand the distinction between Shape and Circle. Shape is a type, and Circle is a function producing a value of type Shape. The fact that a value constructor is a function means it can be partially applied. This leads to a origin concentric circle function:

originConcentricCircle :: Float -> Shape
originConcentricCircle = Circle $ Point 0 0

This new function can be used to generate a sequence of concentric circles, a la

ghci> map originConcentricCircle [1..10]
[Circle (Point 0.0 0.0) 1.0
,Circle (Point 0.0 0.0) 2.0
,Circle (Point 0.0 0.0) 3.0
,Circle (Point 0.0 0.0) 4.0
,Circle (Point 0.0 0.0) 5.0
,Circle (Point 0.0 0.0) 6.0
,Circle (Point 0.0 0.0) 7.0
,Circle (Point 0.0 0.0) 8.0
,Circle (Point 0.0 0.0) 9.0
,Circle (Point 0.0 0.0) 10.0]

The Shape type may be refined further:

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float |
             Rectangle Point Point
             deriving (Show)

This does require changes to surface.

surface :: Shape -> Float
surface (Circle _ r) = pi * r^2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x1 - x2) + (abs $ y1 - y2)

These value constructors could be exported via

module Shapes
(Point(..)
,Shape(..)
,surface)
where

Alternatively, to just allow the use of Shapes via auxilliary functions, export just Shape and the desired functions:

module Shapes
(Shape
,originConcentricCircle)
where

Record syntax

Instead of a tedious declaration such as

-- first name, last name, age, height, phone number
data TediousPerson = TediousPerson String String Int Float String deriving (Show)
printTediousName :: TediousPerson -> String
printTediousName (TediousPerson f l _ _ _) = f ++ " " ++ l

Instead, define a Person as a record:

data Person = Person { firstName :: String
              ,lastName :: String
              ,age :: Int
              ,height :: Float
              ,phoneNumber :: String
              } deriving (Read, Show, Eq)

This defines, for example, a function firstName:

ghci> :t firstName
firstName :: Person -> String

The author:

me :: Person
me = Person "Kyle" "Isom" 27 69 ""

Alternatively,

 meToo :: Person
 meToo = Person{firstName="Kyle"
               ,lastName="Isom"
               ,age=27
               ,height=69
               ,phoneNumber=""
}

A function for printing names using the record functions:

printName :: Person -> String
printName p = (firstName p) ++ " " ++(lastName p)

Type parameters

Consider a vector:

data Vector a = Vector a a a deriving (Show)

A few operations defined on vectors:

vplus :: (Num t) => Vector t -> Vector t -> Vector t
(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)
vectMult :: (Num t) => Vector t -> t -> Vector t
(Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m)
scalarMult :: (Num t) => Vector t -> Vector t -> t
(Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n

Derived instances

Haskell can often automatically derive support for

  • Eq
  • Show
  • Read
  • Ord
  • Enum
  • Bounded

Typically, a record's fields must satisfy the typeclass as well. In the Person type, all of its fields are members of the Eq typeclass, which allows the Person type to automatically derive Eq.

The read function provided by members of the Read type class converts a string to some value:

ghci> :t read
read :: Read a => String -> a

It does require explicit type annotation if the value can't be inferred from context.

ghci> read "5.0"
ERROR - Unresolved overloading
*** Type       : Read a => a
*** Expression : read "5.0"
ghci> read "5.0" :: Float
5.0

We could read a Person:

readPerson :: Person
readPerson = read "Person {firstName=\"Kyle\", lastName=\"Isom\", age=27, height=69, phoneNumber=\"\"}"

Tags: ,