learn-you-a-haskell-chapter-7

Table of Contents

You A Haskell Chapter 7

Modules

We can import modules by using the import keyword.

import Data.List
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

To import in GHCI:

ghci> :m + Data.List

To import a set of modules in GHCI:

ghci> :m + Data.List Data.Map Data.Set

To import only a few parts of a module:

import Data.List (nub, sort)

To import everything except something:

import Data.List hiding(nub)

We can import qualified as well, which requires typing out the full name:

import qualified Data.Map

We can rename it with as.

import qualified Data.Map as M

Data.List

intersperse takes an element and a list and puts that element in between each pair of elements in that list.

ghci> intersperse '.' "MONKEY"
"M.O.N.K.E.Y"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]

intercalate takes a list of lists and a list and inserts that element between those lists and flattens the lists.

ghci> intercalate " " ["hey","there","guys"]
"hey there guys"
ghci> intercalate [0,0,0] [1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

transpose transposes a list of lists. The columns become the rows and the rows become the columns.

ghci> transpose [1,2,3],[4,5,6],[7,8,9]]
[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hey","there","guys"]
["htg","ehu","yey","rs","e"]

Let’s say we have some list where we want to sum up column-wise.

We can do this:

map sum $ transpose [0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]

foldl' and foldll' are the strict versions of foldl and foldll. Instead of returning a thunk, they calculate intermediate values, which lets them be more effective.

concat flattens a list of lists into a list of elements one level.

ghci> concat ["foo","bar","car"]
"foobarcar"
ghci> concat [3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]

concatMap is the same as first mapping a function to a list and then concatenating the list with concat.

ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]

and takes a list of boolean values and returns True only if all the values in the list are True.

ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False

or is like and, only it returns true if any of the boolean values is True.

ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False

any and all work as you would expect, checking if all conform or any conform.

ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
False
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
True

iterate takes a function and a starting value. It applies the function to the starting value, then it applies that function to the result infinitely.

ghci> take 10 $ iterate (*2) 1
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++  "haha"
["haha","hahahaha","hahahahahaha"]

splitAt takes a number and a list. It splits the list at that many elements, returns a tuple.

ghci> splitAt 3 "heyman"
(
ghci> splitAt 100 "heyman"
(
ghci> splitAt (-3) "heyman"
(
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"

takeWhile takes elements from a list while the predicate is true.

ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "This is a sentence"
"This"

What if we wanted to know the sum of all third powers that are under 10,000?

We can do something like this:

We make an infinite list of third powers, chop them off when they’re greater than 10000, then sum them.

ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361

dropWhile only drops elements while the predicate is true. Once it becomes False, it returns the rest of the list.

ghci> dropWhile (/= ' ') "This is a sentence"
" is a sentence"
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]

Imagine finding out when the stock value of a stock exceeds $1000.

ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile ((val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)

span is like takeWhile, only it returns a pair of lists.

ghci> let (fw, rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ", the rest:" ++ rest
"First word: This, the rest: is a sentence"

break is the opposite, it splits when the predicate is first true.

ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])

sort sorts a list. They must conform to Ord to be sortable.

ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "This will be sorted soon"
"    Tbdeehiillnooorssstw"

group takes a list and groups adjacent elements into sublists if they are equal.

ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]

We can sort and map to find out how many times each element appears in the list.

ghci> map (l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]

inits and tails are like init and tail, but they apply recursively until there’s nothing left.

ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[(]

isInfixOf searches for a sublist inside a list and returns True if the sublist we’re looking for is somewhere inside the target list.

ghci> "cat" `isInfixOf` "im a cat burglar"
True
ghci> "Cat" `isInfixOf` "im a cat burglar"
False
ghci> "cats" `isInfixOf` "im a cat burglar"
False

isPrefixOf and isSuffixOf check for a sublist at the beginning and end of a list.

ghci> "hey" `isPrefixOf` "hey there!"
True
ghci> "hey" `isPrefixOf` "oh hey there!"
False
ghci> "there!" `isSuffixOf` "oh hey there!"
True
ghci> "there!" `isSuffixOf` "oh hey there"
False

elem and notElem takes a list and a predicate and returns a pair of lists.

The first list in the result contains all the elements that satisfy the predicate, the second one contains all the ones that don’t.

ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
(
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])

partition takes a list and a predicate and returns a pair of lists. The first list in the result contains all the elements that satisfy the predicate, while the second one contains all the ones that don’t.

ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
(
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])

find takes a list and a predicate and returns the first element that satisfies the predicate.

It returns this in a Maybe type, as the value can be Just something or Nothing.

ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a

elemIndex is like elem, but it returns a Maybe with the index.

ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing

elemIndices returns a list of indices where the element may crop up.

ghci> ' ' `elemIndices` "Where are the spaces?"

findIndex/ findIndices returns the index/indices that match.

ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
[0,6,10,14]

There’s zip3, zipWith3, for when you want to zip three lists or apply a function to a zip.

lines deals with files and takes a string and returns every line of that string in a separate list.

ghci> lines "first linensecond linenthird line"
["first line","second line","third line"]

unlines is the inverse of lines. It takes a list of strings and merges them using ‘n’.

ghci> unlines ["first line", "second line", "third line"]
"first linensecond linenthird linen"

words and unwords are for splitting a line of text into words or joining a list of words into a text.

ghci> words "hey these are the words in this sentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> words "hey these           are    the words in thisnsentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> unwords ["hey","there","mate"]
"hey there mate"

nub returns the unique elements of a list.

ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Lots of words and stuff"
"Lots fwrdanu"

delete takes an element and a list and deletes the first occurence of that element in the list.

ghci> [1..10]  [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Im a big baby"  "big"
"Im a  baby"

union appends every element that doesn’t appear in $f1 to it.

ghci> "hey man" `union` "man what's up"
"hey manwt'sup"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]

intersect works like set intersection.

[1..7] `intersect` [5..10]

insert takes an element and a list of elements and inserts it into a position where it’s still less than or equal to the next element.

ghci> insert 4 [3,5,1,2,8,2]
[3,4,5,1,2,8,2]

insert on a sorted list keeps it sorted.

ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]

genericLength, genericTake, genericDrop, genericSplitAt, genericIndex and genericReplicate are generic functions.

if you want to provide a function as a comparator, we have nubBy, deleteBy, unionBy, intersectBy and groupBy.

if we import on from Data.Function, we can split values on a group like this:

ghci> groupBy ((==) `on` (> 0)) values
[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

on is used a lot with By functions.

ghci> let xs = [5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]

Data.Char

isControl checks whether a character is a control character.

isSpace checks whether a character is a white-space characters. That includes spaces, tab characters, newlines, etc.

isLower checks whether a character is lower-cased.

isUpper checks whether a character is upper-cased.

isAlpha checks whether a character is a letter.

isAlphaNum checks whether a character is a letter or a number.

isPrint checks whether a character is printable. Control characters, for instance, are not printable.

isDigit checks whether a character is a digit.

isOctDigit checks whether a character is an octal digit.

isHexDigit checks whether a character is a hex digit.

isLetter checks whether a character is a letter.

isMark checks for Unicode mark characters. Those are characters that combine with preceding letters to form letters with accents. Use this if you are French.

isNumber checks whether a character is numeric.

isPunctuation checks whether a character is punctuation.

isSymbol checks whether a character is a fancy mathematical or currency symbol.

isSeparator checks for Unicode spaces and separators.

isAscii checks whether a character falls into the first 128 characters of the Unicode character set.

isLatin1 checks whether a character falls into the first 256 characters of Unicode.

isAsciiUpper checks whether a character is ASCII and upper-case.

isAsciiLower checks whether a character is ASCII and lower-case.

Data.Char also exports GeneralCategory, which is like ordering, but it has 31 categories, like Space, UppercaseLetter, LowercaseLetter, OtherPunctuation, DecimalNumber.

toUpper converts a character to upper-case. Spaces, numbers, and the like remain unchanged.

toLower converts a character to lower-case.

toTitle converts a character to title-case. For most characters, title-case is the same as upper-case.

digitToInt converts a character to an Int. To succeed, the character must be in the ranges ‘0’..’9’, ‘a’..’f’ or ‘A’..’F’.

intToDigit is the inverse function of digitToInt. It takes an Int in the range of 0..15 and converts it to a lower-case character.

The ord and chr functions convert characters to their corresponding numbers and vice versa:

Data.Map

To use Data.Map, we import it:

import qualified Data.Map as Map

fromList takes an association list and returns a map.

ghci> Map.fromList [(]
fromList [(]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]

Duplicate keys are discarded.

empty represents an empty map.

insert takes a key, a value and a map and returns a new map, but inserts the key and value to the map.

ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100  Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]

null checks if a map is empty.

ghci> Map.null Map.empty
True
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False

size reports the size of a map.

ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5

singleton takes a key and a value and creates a map with exactly one mapping.

ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]

lookup returns a Maybe, searching for the value through a key.

member takes a value and returns a boolean if the key is in the map or not.

ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False

map and filter work like a list.

toList turns a map to a list.

keys and elems return lists of keys and values.

fromListWith uses a function supplied to it to decide what to do with them.

 phoneBook =
    [(
    ,(
    ,(
    ,(
    ,(
    ,(
    ,(
    ,(
    ,(
    ,(
    ]
phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String
phoneBookToMap xs = Map.fromListWith (number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "betty" $ phoneBookToMap phoneBook
"342-2492, 555-2938"

We can break ties with a function:

ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]

We can break ties by adding values:

ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]

insertWith passes in a function to break ties on a map.

ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]

Data.Set

import qualified Data.Set as Set

Set is like map. We turn lists to sets using Set.fromList.

We can use intersection, difference, union, null, size, member, empty, singleton, insert, and delete like you would expect.

or we can map or filter.

Making our own Modules

Let’s make our own module, Geometry.hs.

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

cubeVolume :: Float -> Float
cubeVolume side = cuboidVolume side side side

cubeArea :: Float -> Float
cubeArea side = cuboidArea side side side

cuboidVolume :: Float -> Float -> Float -> Float
cuboidVolume a b c = rectangleArea a b * c

cuboidArea :: Float -> Float -> Float -> Float
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

We can import this module in the same folder like this:

import Geometry.

We could split this up into three modules, by placing them in a Geometry folder and making three different files.

Sphere.hs

module Geometry.Sphere
( volume
, area
) where

volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)

area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)

Cuboid.hs

module Geometry.Cuboid
( volume
, area
) where

volume :: Float -> Float -> Float -> Float
volume a b c = rectangleArea a b * c

area :: Float -> Float -> Float -> Float
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

Cube.hs

module Geometry.Cube
( volume
, area
) where

import qualified Geometry.Cuboid as Cuboid

volume :: Float -> Float
volume side = Cuboid.volume side side side

area :: Float -> Float
area side = Cuboid.area side side side

And importing like so:

import qualified Geometry.Sphere as Sphere
import qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube

Prev: learn-you-a-haskell-chapter-6 Next: learn-you-a-haskell-chapter-8