An Elm module is a collection of related functions and types. An Elm program is a collection of modules where the main module loads up the other modules and then uses the functions defined in them to do something. Having code split up into several modules has quite a lot of advantages. If a module is generic enough, the functions it exports can be used in a multitude of different programs. If your own code is separated into self-contained modules which don’t rely on each other too much (we also say they are loosely coupled), you can reuse them later on. It makes the whole deal of writing code more manageable by having it split into several parts, each of which has some sort of purpose.
The Elm core library is split into modules, each of them contains functions and types that are somehow related and serve some common purpose. There’s a module for manipulating lists, a module for concurrent programming, a module for dealing with dates, etc. All the functions and types that we’ve dealt with so far were from modules which are imported by default. In this chapter, we’re going to examine a few useful modules and the functions that they have. But first, we’re going to see how to import modules.
The syntax for importing modules in an Elm script is import <module
name>
, which will make the entire module accessible under the module name
namespace (e.g. if we import the List
module, we can access everything exposed
by the module as long as you put List.
before it… like List.map
, List.filter
,
etc.). These are called qualified imports. The functions you’re importing are
qualified by the module name, like Dict.map
or String.toList
You can also import specific functions from a module like this:
import List exposing (map, filter)
or import Dict exposing (..)
(which exposes everything in the Dict
module). These are unqualified imports.
This can make your code more concise, but since many modules export functions
with identical names (e.g. map
and filter
), you may need to take
care not to import multiple identically named functions into the same
module (but don’t worry too much, if you do the compiler will let you know),
Importing must be done before defining any functions, so imports are
usually done at the top of the file. One script can, of course, import
several modules. Just put each import statement into a separate line.
Let’s import the List
module, which has a bunch of useful functions
for working with lists and use a function that it exports to create a
function that tells us how many odd elements a list has. We’ll also import
the Tuple
module to work with the results produced by List.partition
.
List
and Tuple
are imported qualified by default, so the following is
intended just to give you an idea of how importing works. You wouldn’t
necessarily need to do the following in real life.
import List exposing (length, partition)
import Tuple
numOdds : List Int -> Int
numOdds =
let
odd n = n % 2 == 1
in
length << Tuple.first << partition odd
When you do import Tuple
, all the functions that Tuple
exports
become available under the Tuple
namespace, meaning that you can call them
from wherever in the script. And when you do
import List exposing (length, partition)
, you pull length
and partition
into the module unqalified.
partition
is a function defined in List
that
takes a predicate and a list and produces a tuple of two lists. The first list
being those elements for which the predicate evaluated to True
, and the
second list being those elements for which the predicate evaluated to
False
. Composing length
, Tuple.first
, and partition
we produce a function that’s the equivalent of \xs -> length (Tuple.first (partition (\n -> n % 2 == 1) xs))
We can also import modules using the as
keyword to provide a shorter
name. For example,
import Json.Decode as Decode
Decode.string "JSON string"
This makes it so that if we want to reference Json.Decode
’s string
function, we don’t have to type out Json.Decode.string
every time.
Use this handy reference to see which modules are in the standard library. A great way to pick up new Elm knowledge is to just click through the standard library reference and explore the modules and their functions. You can also view the Elm source code for each module. Reading the source code of some modules is a really good way to learn Elm and get a solid feel for it.
The List
module is all about lists, obviously. It provides some
very useful functions for dealing with them. We’ve already met some of
its functions (like map
and filter
) because the List
module is
imported by default. Let’s take a look at some of the functions that we haven’t
met before.
intersperse
takes an element and a list and then puts that element in
between each pair of elements in the list. Here’s a demonstration:
> List.intersperse "on" ["turtles","turtles","turtles"]
["turtles","on","turtles","on","turtles"] : List String
> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6] : List number
concat
flattens a list of lists into just a list of elements.
> List.concat [["foo", "bar"],["baz","qux"]]
["foo","bar","baz","qux"] : List String
> List.concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1] : List number
It will just remove one level of nesting. So if you want to completely
flatten [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]]
, which is a list of lists of
lists, you have to concatenate it twice.
Doing concatMap
is the same as first mapping a function to a list and
then concatenating the list with concat.
> List.concatMap (List.repeat 4) [1,2,3]
[1,1,1,1,2,2,2,2,3,3,3,3] : List number
any
and all
take a predicate and then check if any or all the elements
in a list satisfy the predicate, respectively.
> List.any ((==) 4) [2,3,5,6,1,4]
True : Bool
> List.all ((>) 4) [6,9,10]
True : Bool
> List.all (\n -> n % 2 == 1) [1,2,3,4,5]
False : Bool
> List.any (\n -> n % 2 == 1) [1,2,3,4,5]
True : Bool
sort
simply sorts a list. The type of the elements in the list has to be
part of the comparable
typeclass, because if the elements of a list can’t be
compared in some way, then the list can’t be sorted.
> List.sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
> List.sort ["foo", "bar", "baz", "qux"]
["bar","baz","foo","qux"] : List String
member
checks if an element is inside a list.
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, the second contains all the ones that don’t.
> List.partition (\n -> n % 2 == 1) [1,2,3,4,5,6]
([1,3,5],[2,4,6]) : ( List Int, List Int )
> List.partition (flip (>) 3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3]) : ( List number, List number )
What length
, take
, drop
, repeat
have in common is
that they take an Int as one of their parameters (or return an Int).
For instance, length has a type signature of length : List a -> Int
. If we
try to get the average of a list of numbers by doing let xs = List.range 1 6 in
List.sum xs / List.length xs
, we get a type error, because you can’t use /
with an
Int.
The sort
function also has a more general equivalent.
sortWith
take a function that determines if one element is greater,
smaller or equal to the other. The type signature of sortWith
is sortWith :
(a -> a -> Order) -> List a -> List a
. If you remember from before, the
Order type can have a value of LT, EQ or GT. sort
is the equivalent
of List.sortWith compare
, because compare
just takes two elements whose type is
in the comparable
typeclass and returns their ordering relationship.
Lists can be compared, but when they are, they are compared
lexicographically. What if we have a list of lists and we want to sort
it not based on the inner lists’ contents but on their lengths? Well, as
you’ve probably guessed, we’ll use the sortWith
function. First we’ll
define the on
function, like this, to act as a helper:
on : (b -> b -> c) -> (a -> b) -> a -> a -> c
on f g = \x y -> f (g x) (g y)
So doing on (==) (> 0)
returns an equality function that looks like \x y -> (x > 0) == (y > 0)
. on
is useful with the By/With functions because with it, we can do:
> xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
[[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]] : List (List number)
> List.sortWith (on compare List.length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]
Awesome! on compare List.length
… man, that reads almost like real
English! If you’re not sure how exactly the on
works here, on compare List.length
is the equivalent of \x y -> compare (List.length x) (List.length y)
.
When you’re dealing with By/With functions that take an equality
function, you might do something like on (==) something
and when you’re dealing
with By/With functions that take an ordering function, you might do
on compare something
.
The Char
module does what its name suggests. It exports functions
that deal with characters.
Char
exports a bunch of predicates over characters. That is,
functions that take a character and tell us whether some assumption
about it is true or false. Here’s what they are:
isUpper
checks whether a character is an upper case ASCII letter.
isLower
checks whether a character is a lower case ASCII letter.
isDigit
checks whether a character is an ASCII digit [0-9].
isOctDigit
checks whether a character is an ASCII octal digit [0-7].
isHexDigit
checks whether a character is an ASCII hexadecimal digit [0-9a-fA-F].
All these predicates have a type signature of Char -> Bool
. Most of the
time you’ll use this to filter out strings or something like that. For
instance, let’s say we’re making a program that takes a username and the
username can only be comprised of lowercase characters. We can use
the List
function all
and the String
function toList
in combination
with the Char
predicates to determine if the username is alright.
> List.all Char.isLower <| String.toList "bobby"
True : Bool
> List.all Char.isLower <| String.toList "Bobbyå"
False : False
Kewl. In case you don’t remember, all
takes a predicate and a list and
returns True
only if that predicate holds for every element in the list.
toUpper
converts a character to upper-case. Spaces, numbers, and the
like remain unchanged.
toLower
converts a character to lower-case.
toCode
converts a character to a KeyCode
(a type alias for Int
).
> List.map Char.toCode <| String.toList "34538"
[51,52,53,51,56] : List Char.KeyCode
> List.map Char.toCode <| String.toList "FF85AB"
[70,70,56,53,65,66] : List Char.KeyCode
fromCode
is the inverse function of toCode
. It takes a KeyCode
and converts it to a character.
> Char.fromCode 70
'F' : Char
> Char.toCode 51
'3'
The Caesar cipher is a primitive method of encoding messages by shifting each character in them by a fixed number of positions in the alphabet. We can easily create a sort of Caesar cipher of our own, only we won’t constrict ourselves to the alphabet.
encode : Int -> String -> String
encode shift msg =
let
ords = List.map Char.toCode <| String.toList msg
shifted = List.map ((+) shift) ords
in
String.fromList <| List.map Char.fromCode shifted
Here, we first convert the string to a list of Char
, then map
it to a list of KeyCode
’s (which, again, are just a type alias to Int
).
Then we add the
shift amount to each number before converting the list of numbers back
to characters, then back to a string. If you’re a composition cowboy,
you could write the body of this function as
String.fromList <| List.map (Char.fromCode << ((+) shift) << Char.toCode)
<| String.toList msg
.
Let’s try encoding a few messages.
> encode 3 "Heeeeey"
"Khhhhh|" : String
> encode 4 "Heeeeey"
"Liiiii}" : String
> encode 1 "abcd"
"bcde" : String
> encode 5 "Marry Christmas! Ho ho ho!"
"Rfww~%Hmwnxyrfx&%Mt%mt%mt&" : String
That’s encoded alright. Decoding a message is basically just shifting it back by the number of places it was shifted by in the first place.
decode : Int -> String -> String
decode shift msg = encode (negate shift) msg
> encode 3 "Im a little teapot"
"Lp#d#olwwoh#whdsrw"
> decode 3 "Lp#d#olwwoh#whdsrw"
"Im a little teapot"
> decode 5 << encode 5 <| "This is a sentence"
"This is a sentence"
Association lists (also called dictionaries) are lists that are used to store key-value pairs where ordering doesn’t matter. For instance, we might use an association list to store phone numbers, where phone numbers would be the values and people’s names would be the keys. We don’t care in which order they’re stored, we just want to get the right phone number for the right person.
The most obvious way to represent association lists in Elm would be by having a list of pairs. The first component in the pair would be the key, the second component the value. Here’s an example of an association list with phone numbers:
phoneBook =
[("betty","555-2938")
,("bonnie","452-2928")
,("patsy","493-2928")
,("lucille","205-2928")
,("wendy","939-8282")
,("penny","853-2492")
]
Despite this seemingly odd indentation, this is just a list of pairs of strings. The most common task when dealing with association lists is looking up some value by key. Let’s make a function that looks up some value given a key.
findKey : k -> List (k,v) -> Maybe v
findKey key xs = Maybe.map Tuple.second << List.head << List.filter (\(k,v) -> key == k) <| xs
Pretty simple. The function that takes a key and a list, filters the
list so that only matching keys remain, gets the first key-value that
matches and returns the value. But what happens if the key we’re looking
for isn’t in the association list? Hmm. Well, our old friend Maybe
is
here to help. If we don’t find the key, we’ll return a Nothing
.
If we find it, we’ll return Just something
, where something
is the value
corresponding to that key.
This is a textbook recursive function that operates on a list. Edge case, splitting a list into a head and a tail, recursive calls, they’re all there. This is the classic fold pattern, so let’s see how this would be implemented as a fold.
findKey : k -> List (k,v) -> Maybe v
findKey key = List.foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
Note: It’s usually better to use folds for this standard list
recursion pattern instead of explicitly writing the recursion because
they’re easier to read and identify. Everyone knows it’s a fold when
they see the foldr
call, but it takes some more thinking to read
explicit recursion.
> findKey "penny" phoneBook
Just "853-2492" : Maybe.Maybe String
> findKey "betty" phoneBook
Just "555-2938" : Maybe.Maybe String
> findKey "wilma" phoneBook
Nothing : Maybe.Maybe String
Works like a charm! If we have the girl’s phone number, we Just
get the
number, otherwise we get Nothing
.
If we want to find the corresponding value to a key, we have to traverse all the
elements of the list until we find it. The Dict
module offers
association lists that are much faster (because they’re internally
implemented with trees) and also it provides a lot of utility functions.
From now on, we’ll say we’re working with dictionaries instead of association
lists.
Let’s go ahead and see what Dict
has in store for us! Here’s the
basic rundown of its functions.
The fromList
function takes an association list (in the form of a list)
and returns a dictionary with the same associations.
> Dict.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
Dict.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")] : Dict.Dict String String
> Dict.fromList [(1,2),(3,4),(3,2),(5,5)]
Dict.fromList [(1,2),(3,2),(5,5)] : Dict.Dict number number1
If there are duplicate keys in the original association list, the
duplicates are just discarded. This is the type signature of fromList
Dict.fromList : List (comparable, v) -> Dict.Dict comparable v
It says that it takes a list of pairs of type comparable
and v
and returns a
dictionary that maps from keys of type comparable
to type v
.
Notice here that the keys have to be comparable. That’s an essential
constraint in the Dict
module. It
needs the keys to be comparable so it can arrange them in a tree.
You should always use Dict
for key-value associations unless you
have keys that aren’t part of the comparable
typeclass.
Let’s back up for a moment. Did notice anything unusual with our
dictionary’s type signature? Dict.Dict number number1
. We’ve seen
number
before, so what is number1
? Well, number
in Elm is a
typeclass consisting of either Int
or Float
. So what the type
system is telling us is that we have a dictionary with keys of
type number
and values also of type number
, but we should not
assume that these types are equivalent! It could turn out that our
keys are Int
’s, but our values are Float
’s (or vice versa), so
the type system is helping us out here by keeping these two types
distinct. Alright, let’s continue.
empty
represents an empty dictionary. It takes no arguments, it just returns an
empty dictionary.
> Dict.empty
Dict.fromList [] : Dict.Dict k v
insert
takes a key, a value and a dictionary and returns a new dictionary
that’s just like the old one, only with the key and value inserted.
> Dict.empty
Dict.fromList [] : Dict.Dict k v
> Dict.insert 3 100 Dict.empty
Dict.fromList [(3,100)] : Dict.Dict number number1
> Dict.insert 5 600 (Dict.insert 4 200 ( Dict.insert 3 100 Dict.empty))
fromList [(3,100),(4,200),(5,600)] : Dict.Dict number number1
> Dict.insert 5 600 << Dict.insert 4 200 << Dict.insert 3 100 <| Dict.empty
fromList [(3,100),(4,200),(5,600)] : Dict.Dict number number1
We can implement our own fromList
by using the empty dictionary, insert
and a
fold
. Watch:
fromList : List (comparable,v) -> Dict.Dict comparable v
fromList = List.foldr (\(k, v) acc -> Dict.insert k v acc) Dict.empty
It’s a pretty straightforward fold. We start of with an empty dictionary and we fold it up from the right, inserting the key value pairs into the accumulator as we go along.
isEmpty
checks if a dictionary is empty.
> Dict.isEmpty Dict.empty
True : Bool
> Dict.isEmpty <| Dict.fromList [(2,3),(5,5)]
False : Bool
size
reports the size of a dictionary.
> Dict.size Dict.empty
0 : Int
> Dict.size <| Dict.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5 : Int
singleton
takes a key and a value and creates a dictionary that has exactly one
mapping.
> Dict.singleton 3 9
fromList [(3,9)] : Dict.Dict number number1
> Dict.insert 5 9 <| Dict.singleton 3 9
fromList [(3,9),(5,9)] : Dict.Dict number number1
get
returns Just something
if it finds something
for the key and Nothing
if it doesn’t.
member
is a predicate that takes a key and a dictionary and reports whether the key
is in the dictionary or not.
> Dict.member 3 <| Dict.fromList [(3,6),(4,3),(6,9)]
True : Bool
> Dict.member 3 <| Dict.fromList [(2,5),(4,5)]
False : Bool
map
and filter
work much like their list equivalents, although the functions
you pass to them will need to have a slightly different signature to account
for both the key and the value which they will be passed.
> Dict.map (\k v -> (k, v * 100)) <| Dict.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)] : Dict.Dict number number1
> Dict.filter (\_ v -> Char.isUpper v) <| Dict.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')] : Dict.Dict number Char
toList
is the inverse of fromList
.
> Dict.toList << Dict.insert 9 2 <| Dict.singleton 4 3
[(4,3),(9,2)] : List (number, number)
keys
and values
return lists of keys and values respectively. keys
is the
equivalent of List.map Tuple.first << Dict.toList
and values
is the equivalent of
List.map Tuple.second << Dict.toList
These were just a few functions from Dict
. You can see a complete
list of functions in the
documentation.
The Set
module offers us, well, sets. Like sets from mathematics.
Sets are kind of like a cross between lists and dictionaries. All the elements
in a set are unique. And because they’re internally implemented with
trees (much like dictionaries in Dict
), they’re ordered. Checking for
membership, inserting, deleting, etc. is much faster than doing the same
thing with lists. The most common operation when dealing with sets are
inserting into a set, checking for membership and converting a set to a
list.
Let’s say we have two pieces of text. We want to find out which characters were used in both of them.
text1 = "I just had an anime dream. Anime... Reality... Are they so different?"
text2 = "The old man left his garbage can out and now his trash is all over my lawn!"
The fromList function works much like you would expect. It takes a list and converts it into a set.
> set1 = Set.fromList <| String.toList text1
Set.fromList [' ','.','?','A','I','R','a','d','e','f','h','i','j','l','m','n','o','r','s','t','u','y']
: Set.Set Char
> set2 = Set.fromList <| String.toList text2
Set.fromList [' ','!','T','a','b','c','d','e','f','g','h','i','l','m','n','o','r','s','t','u','v','w','y']
: Set.Set Char
As you can see, the items are ordered and each element is unique. Now
let’s use the intersect
function to see which elements they both
share.
> Set.intersect set1 set2
Set.fromList [' ','a','d','e','f','h','i','l','m','n','o','r','s','t','u','y']
: Set.Set Char
We can use the diff
function to see which letters are in the first
set but aren’t in the second one and vice versa.
> Set.diff set1 set2
Set.fromList ['.','?','A','I','R','j'] : Set.Set Char
> Set.diff set2 set1
Set.fromList ['!','T','b','c','g','v','w'] : Set.Set Char
Or we can see all the unique letters used in both sentences by using
union
.
> Set.union set1 set2
Set.fromList [' ','!','.','?','A','I','R','T','a','b','c','d','e','f','g','h','i','j','l','m','n','o','r','s','t','u','v','w','y']
: Set.Set Char
The isEmpty
, size
, member
, empty
, singleton
, insert
and remove
functions
all work like you’d expect them to.
> Set.isEmpty Set.empty
True : Bool
> Set.isEmpty <| Set.fromList [3,4,5,5,4,3]
False : Bool
> Set.size <| Set.fromList [3,4,5,3,4,5]
3 : Int
> Set.singleton 9
Set.fromList [9] : Set.Set number
> Set.insert 4 <| Set.fromList [9,3,8,1]
Set.fromList [1,3,4,8,9] : Set.Set number
> Set.insert 8 <| Set.fromList <| List.range 5 10
Set.fromList [5,6,7,8,9,10] : Set.Set Int
> Set.remove 4 <| Set.fromList [3,4,5,4,3,4,5]
Set.fromList [3,5] : Set.Set number
We can also map over sets and filter them.
> odd n = n % 2 == 1
<function> : Int -> Bool
> Set.filter odd <| Set.fromList [3,4,5,6,7,2,3,4]
Set.fromList [3,5,7] : Set.Set Int
> Set.map ((+) 1) <| Set.fromList [3,4,5,6,7,2,3,4]
Set.fromList [3,4,5,6,7,8] : Set.Set number
Sets are often used to weed a list of duplicates from a list by first
making it into a set with fromList
and then converting it back to a list
with toList
.
> setDedup xs = Set.toList <| Set.fromList xs
<function> : List comparable -> List comparable
> String.fromList <| setDedup <| String.toList "HEY WHATS CRACKALACKIN"
" ACEHIKLNRSTWY" : String
Keep in mind, though, that the order of the original list likely will not be preserved.
We’ve looked at some cool modules so far, but how do we make our own module? Almost every programming language enables you to split your code up into several files and Elm is no different. When making programs, it’s good practice to take functions and types that work towards a similar purpose and put them in a module. That way, you can easily reuse those functions in other programs by just importing your module.
Let’s see how we can make our own modules by making a little module that provides some functions for calculating the volume and area of a few geometrical objects. We’ll start by creating a file called Geometry.elm.
We say that a module exports functions. What that means is that when I import a module, I can use the functions that it exports. It can define functions that its functions call internally, but we can only see and use the ones that it exports.
At the beginning of a module, we specify the module name. If we have a file called Geometry.elm, then we should name our module Geometry. Then, we specify the functions that it exports and after that, we can start writing the functions. So we’ll start with this.
module Geometry
exposing
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
)
As you can see, we’ll be doing areas and volumes for spheres, cubes and cuboids. Let’s go ahead and define our functions then:
module Geometry
exposing
( cubeArea
, cubeVolume
, cuboidArea
, cuboidVolume
, sphereArea
, sphereVolume
)
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
Pretty standard geometry right here. There are a few things to take note
of though. Because a cube is only a special case of a cuboid, we defined
its area and volume by treating it as a cuboid whose sides are all of
the same length. We also defined a helper function called rectangleArea
,
which calculates a rectangle’s area based on the lenghts of its sides.
It’s rather trivial because it’s just multiplication. Notice that we
used it in our functions in the module (namely cuboidArea
and
cuboidVolume
) but we didn’t export it! Because we want our module to
just present functions for dealing with three dimensional objects, we
used rectangleArea
but we didn’t export it.
When making a module, we usually export only those functions that act as
a sort of interface to our module so that the implementation is hidden.
If someone is using our Geometry
module, they don’t have to concern
themselves with functions that we don’t export. We can decide to change
those functions completely or delete them in a newer version (we could
delete rectangleArea
and just use *
instead) and no one will mind
because we weren’t exporting them in the first place.
To use our module, we just do:
import Geometry
Geometry.elm has to be in the same folder that the program that’s importing it is in, though.
Modules can also be given a hierarchical structures. Each module can
have a number of sub-modules and they can have sub-modules of their own.
Let’s section these functions off so that Geometry
is a module that has
three sub-modules, one for each type of object.
First, we’ll make a folder called Geometry. Mind the capital G. In it, we’ll place three files: Sphere.elm, Cuboid.elm, and Cube.elm. Here’s what the files will contain:
Sphere.elm
module Geometry.Sphere
exposing
( area
, volume
)
volume : Float -> Float
volume radius =
(4.0 / 3.0) * pi * (radius ^ 3)
area : Float -> Float
area radius =
4 * pi * (radius ^ 2)
Cuboid.elm
module Geometry.Cuboid
exposing
( area
, volume
)
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.elm
module Geometry.Cube
exposing
( area
, volume
)
import Geometry.Cuboid as Cuboid
volume : Float -> Float
volume side =
Cuboid.volume side side side
area : Float -> Float
area side =
Cuboid.area side side side
Alright! So first is Geometry.Sphere
. Notice how we placed it in a
folder called Geometry and then defined the module name as
Geometry.Sphere
. We did the same for the cuboid. Also notice how in all
three sub-modules, we defined functions with the same names. We can do
this because they’re separate modules.
So now if we’re in a file that’s on the same level as the Geometry folder, we can do, say:
import Geometry.Sphere
And then we can call area
and volume
and they’ll give us the area and
volume for a sphere.
The next time you find yourself writing a file that’s really big and has a lot of functions, try to see which functions serve some common purpose and then see if you can put them in their own module. You’ll be able to just import your module the next time you’re writing a program that requires some of the same functionality.