- Values and functions (& basic types)
If you want to run the examples, I recommend installing stack
via the link found in the README and
using our ready-made template for small applications: stack new project-name quanterall/basic
.
This will give you an already set-up project and you can put the example code in the Library.hs
file.
If you are so inclined, you can use development containers to get a functioning development environment that I've set up; the prompt should pop up when you open the project. If you don't feel like you want this, you can install the "Simple GHC (Haskell) Integration" (extension ID dramforever.vscode-ghc-simple`) in VSCode for a setup that is known to work. When you save you should be able to get compiler warnings and errors.
Most examples have (and should have) the needed imports in the examples themselves, so if you find one that doesn't, please let me know.
If you want to run the examples interactively you can run stack repl
in the root directory of the
generated project and it will start an interactive session where you can execute the functions that
have been defined, as well as all the standard library. If you want to make changes and see these
reflected in the session, you can issue the :r
command in it and it will reload the project files.
The bread and butter of a Haskell program is of course values and functions.
Before a value or a function definition comes the type of the definition. This is by convention to a large degree, we don't actually have to specify types for a lot of things depending on whether or not the compiler can figure them out itself. Even so, most projects will be set up to warn you when a top-level definition doesn't have an explicit type for the simple reason that it's part of good documentation to specify the intended type.
A type signature is written with the name of the thing it's for, followed by ::
and then the type:
myValue :: Int
The actual definition for it comes immediately after and is written with =
to signify that the
name on the left is equal to the expression on the right:
myValue :: Int
myValue = 42
The basic types in Haskell:
import Prelude
myAnswerToEverything :: Int -- Integer, limited in size
myAnswerToEverything = 42
myInteger :: Integer -- Integer, can grow as needed
myInteger = 42
myChar :: Char -- A single character
myChar = '$'
myFloat :: Float -- 32-bit floating point value
myFloat = 1337.0
myDouble :: Double -- 64-bit floating point value
myDouble = 1337.0
myBool :: Bool -- `True` or `False`
myBool = False
myString :: String -- Actually just a type alias for a list of characters; `[Char]`
myString = "This is not the best string in the world, it's just a tribute"
-- `String`s also handle "exotic" script
helloThere :: String
helloThere = "Ехо... Генерал Кеноби"
-
Define a value with the name
myOwnValue
with the typeDouble
and the value42
. -
Define a value with the type
String
and the value['h', 'e', 'l', 'l', 'o']
, then a value with the typeString
and the value"hello"
. Why do both of these work? -
Define a value with the type
String
and the value'hello'
. What happens and why do you think that is?
Functions are written much like values, but have arrows in their type. The types, starting at the leftmost, represent the arguments/parameters passed to the function, with the final (rightmost) one representing the return value.
import Prelude
-- a b result
addInts :: Int -> Int -> Int
addInts a b = a + b
Calling functions, as we can see above, can be done via infix notation when they are operators. We could also call the operator in a prefix position:
import Prelude
-- a b result
addInts :: Int -> Int -> Int
addInts a b = (+) a b
The application of a function is done by just writing the function name followed by a space and then spaces between the parameters, making it as lightweight an operation as possible.
import Prelude
-- x divisor result
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- When we want to divide our problem into smaller parts we can use `let`.
-- `rem` here is a function that takes an integer and returns the remainder of dividing it by the
-- second argument. It is the same as the `%` operator (modulo) from C, JavaScript, and so on.
let remainderOfDivision = rem x divisor
-- `let` has to be followed by `in` and then the final expression that is to be executed.
in remainderOfDivision == 0
rem
in the above code snippet is a function that takes an integer-like number and returns the
remainder of dividing it by the second argument. Here we check whether or not that division returns
0 to determine if it was divisible by it.
We could also write the division as follows:
import Prelude
-- x divisor result
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- Note how we surround the function in backticks (`) to be able to put it in the infix position.
let remainderOfDivision = x `rem` divisor
in remainderOfDivision == 0
This can be used to make code read more intuitively and is very common in the case of checking if something is a member of a map, list or the like:
key `Map.member` ourMap -- Is the key defined in the map?
element `List.elem` ourList -- Is the element present in the list?
We can use this with any function, even functions with more than two arguments. It's important to recognize that it's not always a great idea to use this feature and each case should be examined on an individual basis in terms of whether or not it makes the code more or less easy to understand.
Math | Haskell | Notes |
---|---|---|
+ | + | |
- | - | |
* | * | |
/ | / | |
> | > | |
< | < | |
≥ | >= | |
≤ | <= | |
= | == | |
≠ | /= | |
xⁿ | x ^ n | x is numeric, n is integral |
xⁿ | x ** n | x & n are both floating point values |
x mod n | x rem n |
x and n are integral [0] |
x mod n | x mod n |
x and n are integral [0] |
Math | C | Haskell | Notes |
---|---|---|---|
¬ | ! | not | |
∧ | && | && | |
∨ | ǀǀ | ǀǀ | Two pipe characters |
[0]:
-- Note: Use `rem` if you know your parameters are both positive
5 `mod` 3 == 2
5 `rem` 3 == 2
5 `mod` (-3) == -1
5 `rem` (-3) == 2
(-5) `mod` 3 == 1
(-5) `rem` 3 == -2
(-5) `mod` (-3) == -2
(-5) `rem` (-3) == -2
Arithmetic can only be done with numbers of the same type, so what happens when we want to divide an integer number with a floating point number? Let's see:
-- integer float result
divideInteger :: Int -> Float -> Float
-- This is the same as `(fromIntegral x) / f` because `/` divides everything on the left by
-- everything on the right.
-- `fromIntegral` is a function that takes anything integer-like and turns it into a `Float`.
divideInteger integer float = fromIntegral integer / float
Likewise we can also take a float and turn it into an integer, though this can obviously lead to a loss of precision in our calculations:
subtractRoundedFloat :: Int -> Float -> Int
subtractRoundedFloat int float = int - round float
If we were to run this we'd see:
Q> subtractRoundedFloat 5 5.5
-1
Q> subtractRoundedFloat 5 5.4
0
- Define a function that returns whether or not an
Int
is zero.
Q> isZero 1
False
Q> isZero 0
True
- Define a function that returns whether or not a
Float
is greater than zero.
Q> isGreaterThanZero 1.2
True
Q> isGreaterThanZero 0.0
False
- Define a function that adds 1/10th of a Double to itself.
Q> addOneTenth 1.0
1.1
Q> addOneTenth 0.1
0.11000000000000001
- Define a function that takes 2
Float
slength'
&width
and returns the area of the rectangle they make up.
Q> rectangleArea 2.5 3
7.5
Q> rectangleArea 2 2
4.0
- Define a function that takes a radius of type
Float
and returns the area of a circle[0].
Q> circleArea 3.5
38.484512
Q> circleArea 2.1
13.854422
- Define a function
calculateBMI
that takes aFloat
representing weight in kilograms and anInt
representing height in centimeters and returns the person's BMI.
Q> bmi 185 90
26.3
Q> bmi 165 60
22.0
pi
is available by default.
Quite regularly we will have to pose some kind of question about the structure of a value, or whether or not it matches some kind of predicate.
if
in Haskell is a lot like if
in other languages, with the one slight difference to some of
them that both the True
and False
branch need to be present, and both branches need to return
the same type of value. This is because if
, like most other things in Haskell, is an expression
and the result can be bound to a name.
value :: Int -> Int
value x = if x < 10 then x else 10
value' :: Int -> Int
value' x =
if x < 10
then x
else 10
Let's look at a few ways to ask questions about values in the case of "clamping" a number.
import Prelude
-- | Limits a given integer to be within the range @lowerBound <= value <= upperBound@.
clamp :: Int -> Int -> Int -> Int
clamp lowerBound upperBound value
| value < lowerBound = lowerBound
| value > upperBound = upperBound
| otherwise = value
The above is likely the most natural way of doing this in this very example, because we have several questions to ask about the value and we can do so immediately in what are known as "guards".
Note that we do not have an immediate =
after our parameters but instead each |
introduces a new
question that we pose, a new guard. If the boolean expression that follows the pipe (|
)
evaluates to True
the expression to the right of =
is what will be returned.
The word otherwise
is an always matching case and we can use this case as an "for all other cases"
clause.
- Define a function that takes two
Int
s and returns the biggest of the two. Implement it both with function guards as well asif
.
Q> biggest 2 1
2
Q> biggest 1 2
2
Q> biggest 1 (-2)
1
- Define a function that takes two
Int
s and returns the smallest of the two. Implement it both with function guards as well asif
.
Q> smallest 2 1
1
Q> smallest 1 2
1
Q> smallest 1 (-2)
-2
- Define a function that subtracts an integer from another, but if the result is less than zero,
instead return
0
.
Q> subtractPositive 3 1
2
Q> subtractPositive 1 3
0
- Define a function that takes an
Int
and if it's smaller than zero returns0
, if it's bigger than 255 returns255
. Otherwise it returns the integer itself.
Q> clampByteValue 365
255
Q> clampByteValue (-255)
0
Q> clampByteValue 128
128
Sometimes you will see a dollar sign operator ($
) in code. This is actually a utility operator
meant for function application:
f :: Int -> String
f x = show (deriveFlorbFactorFromMerkleNumber (castToMerkleNumber x))
f' :: Int -> String
f' x = show $ deriveFlorbFactorFromMerkleNumber $ castToMerkleNumber x
As you can see from the above snippet, using $
allows us to just say "Apply the function on the
left to the expression on the right". $
binds tightly to the right, so the parentheses we see
above is what will be by default, hence we have no need for parentheses like this when we use $
.
When you apply a function, you can choose to not pass all the arguments it's expecting. This will result in a function that expects the remaining arguments and that will have the same return value:
import Prelude
addInts :: Int -> Int -> Int
addInts a b = a + b
add42 :: Int -> Int
add42 = addInts 42
Passing only one arguments to addInts
in the example above results in a function that expects one
more argument instead of the original two.
It's quite common to use this fact by putting important arguments in the last parameter position of a function in order to let people use this pattern.
In the code snippet above we are using what is called an "anonymous function". This is a function
defined in-place and passed to another function. These are also commonly called "lambdas". A lambda
is written beginning with \
(because it is supposed to resemble λ, an actual lambda), followed by
the arguments that the function takes. After the arguments we have an arrow and what follows is the
expression of the function, i.e. what usually comes after =
in a normal function definition.
import qualified Data.List as List
import Prelude
-- | Adds 42 to every item in a list
add42ToAll :: [Int] -> [Int]
add42ToAll = List.map (\x -> x + 42)
add42ToAll' :: [Int] -> [Int]
add42ToAll' list = List.map (\x -> x + 42) list
In this example we are creating a lambda that takes one argument and adds 42 to it. This is passed
to a partially applied List.map
that takes a list of Int
as the second argument. When we don't
give this argument, what we get back is exactly the type signature that add42ToAll
has:
[Int] -> [Int]
. The second version makes this list argument visible. As a rule, when you have
arguments both on the left side in the same order as you have them on the right side of a definition
you can remove them on both sides. list
appears here as the last argument both in the arguments
and the implementation and so we can get rid of it, to talk only about the essence of the function;
mapping over a list and adding 42 to each item in the list.
We can also partially apply our +
. The function that we are passing to List.map
is expected to
be of type Int -> Int
, which is what we get when we write (+ 42)
:
import qualified Data.List as List
import Prelude
-- | Adds 42 to every item in a list
add42ToAll :: [Int] -> [Int]
add42ToAll = List.map (+ 42) -- could also be `(42 +)`
Since operators expect arguments both on the left and right side we can partially apply whichever
side we want, so (42 +)
is also valid. The resulting behavior obviously depends on the operator,
as an operator like -
would behave differently depending on which side you are omitting.
- Define a function that takes a list of
Int
s and multiplies each with2
. Remembermap
[0].
Q> multiplyAllByTwo [1, 2, 3]
[2, 4, 6]
Q> multiplyAllByTwo []
[]
- Define a function that takes a list of
Int
s and squares each. Remembermap
[0].
Q> squareAll [1, 2, 3]
[1, 4, 9]
Q> squareAll []
[]
Q> squareAll [-1, -2, -3]
[1, 4, 9]
filter
[1] is a function that takes all elements that match a predicate from a list. This predicate is always a function that takes an element in the list and returns a boolean value. Define a function that takes all elements that are equal to0
from a list. Use partial application both withfilter
and when constructing the predicate.
Q> allZeros [0, 1, 0, 2, 0, 3]
[0, 0, 0]
Q> allZeros [1..9]
[]
- Define a function that returns all of the
Int
s in a list over0
, use partial application for both the predicate andfilter
.
Q> numbersAboveZero [0, 1, 0, 2, 0, 3]
[1, 2, 3]
Q> numbersAboveZero [1..9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
- Define a function that takes numbers from a list of
Int
s, stopping when it reaches a number that is above 10.takeWhile
[2] can be useful for this. Use partial application both fortakeWhile
[2] and the predicate you pass to it.
Q> takeBelow10 [5..15]
[5, 6, 7, 8, 9]
Q> takeBelow10 [1..9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Q> takeBelow10 []
[]
- Define a function that checks whether or not all
Int
s in a a list are even. Use notes to figure out how and use partial application for your definition.
Q> areAllEven [2, 4..10]
True
Q> areAllEven [1..9]
False
Q> areAllEven []
True
Partial application is especially useful in pipelines, since we can construct new functions by partially applying other functions, easily creating a function that takes the result of a previous operation and returning a new one, possibly passing it along to yet another function:
import Control.Category ((>>>))
import Data.Function ((&))
import Prelude
-- `/=` is the "not equal" operator in Haskell, analogous to `!=` in many other languages.
-- Note how we're again using an operator with only one argument, and are missing the left-most one,
-- which gives us a function expecting one argument, which is exactly what the predicate we pass to
-- `takeWhile` here expects: `Char -> Bool`
-- Types for this example:
--
-- reverse :: String -> String
-- takeWhile :: (Char -> Bool) -> String -> String
-- length :: String -> Int
dataPartLength :: String -> Int
dataPartLength = length . takeWhile (/= '1') . reverse
dataPartLength' :: String -> Int
dataPartLength' = reverse >>> takeWhile (/= '1') >>> length
dataPartLength'' :: String -> Int
dataPartLength'' string = string & reverse & takeWhile (/= '1') & length
In the above examples we are pipelining functions that operate on the result of a previous function
call. The first example does this using the .
operator, which represents classic function
composition. One thing to note about this is that the application order is read from right to left,
so we are applying reverse
first, then takeWhile
, then length
.
reverse
takes a String
and will reverse it. The result is then passed to takeWhile (/= '1')
which will take all initial characters of the string until we find one that is '1'
. The result of
that is then passed to length
which will return the length.
The example using >>>
does the same thing, but can be read from left to right. The last example,
using the operator &
is the same as commonly used pipeline operators like |>
from F#, Elm &
Elixir, and might be more readable to some. It works by taking whatever value we have on the left
side of it and passing it to the function on the right. Like F# and Elm the value is passed as the
last argument to the function on the right, as opposed to Elixir where it is passed as the first.
While we aren't changing the meaning of our program based on which way we compose our functions, We
should always prefer to read our code left-to-right and up-to-down (because this is the already
established reading direction in the rest of the language), meaning we should use >>>
and
&
.
Let's look at the very first Project Euler as an example of using function composition and partial application to get the answer to a mildly complex question:
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
Let's first look at the given example numbers in action:
import Data.Function ((&))
import Prelude
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- Note how we surround the function in backticks (`) to be able to put it in the infix position.
let remainderOfDivision = x `rem` divisor
in remainderOfDivision == 0
solution :: Int
solution =
[1,2,3,4,5,6,7,8,9] & filter (\x -> x `isDivisibleBy` 3 || x `isDivisibleBy` 5) & sum -- 23
We can use a shorthand plus takeWhile
to make this a bit neater:
import Data.Function ((&))
import Prelude
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- Note how we surround the function in backticks (`) to be able to put it in the infix position.
let remainderOfDivision = x `rem` divisor
in remainderOfDivision == 0
solution :: Int
solution =
-- `[1..]` means "create an infinite list of increasing numbers starting from 1"
-- We never have an infinite list in memory, but rather take elements one by one until we reach 10
[1..] & takeWhile (< 10) & filter (\x -> x `isDivisibleBy` 3 || x `isDivisibleBy` 5) & sum -- 23
If we now set an upper bound as a parameter we can get the solution to the actual problem:
import Data.Function ((&))
import Prelude
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- Note how we surround the function in backticks (`) to be able to put it in the infix position.
let remainderOfDivision = x `rem` divisor
in remainderOfDivision == 0
solution :: Int -> Int
solution upperBound =
-- You could also write `[1..(upperBound - 1)]` and skip `takeWhile`, but for reasons explained
-- later on this doesn't make a difference in the execution of this function.
[1..] & takeWhile (< upperBound) & filter (\x -> x `isDivisibleBy` 3 || x `isDivisibleBy` 5) & sum
-- `solution 1000` will give us the value 233168
If we wanted to support using different divisors we could also do the following:
import Data.Function ((&))
import Prelude
isDivisibleBy :: Int -> Int -> Bool
isDivisibleBy x divisor =
-- Note how we surround the function in backticks (`) to be able to put it in the infix position.
let remainderOfDivision = x `rem` divisor
in remainderOfDivision == 0
solution :: Int -> [Int] -> Int
solution upperBound divisors =
-- `any` takes a predicate/question and a list of inputs and answers the question:
-- "Are any of these true?"/"Do any of these return `True`?"
-- Here we are asking "Is X divisble by any of the passed in divisors?"
-- We could also write `isDivisibleBy x` only, but it can be useful to use backticks to emphasize
-- that `x` is the first argument, and to make it read as a question about `x`.
[1..] & takeWhile (< upperBound) & filter (\x -> any (x `isDivisibleBy`) divisors) & sum
-- `solution 1000 [3, 5]` will give us the value 233168
- Define a function using a pipeline that takes a list of
Int
, gets all the even numbers in it, multiplies them all by two and returns the sum[1]. Define versions that use:- a named argument; with
&
- an unnamed argument; with
>>>
- a named argument; with
Q> multipliedEvenSum [2, 4..10]
60
Q> multipliedEvenSum [1..9]
40
Q> multipliedEvenSum []
0
Q> multipliedEvenSum [1, 3..9]
0
- Define a function using a pipeline that takes a list of
Int
, takes numbers until it finds one that is not even[2], multiplies them all by two, sums them up and returns whether or not the sum is even. Define versions that use:- a named argument; with
&
- an unnamed argument; with
>>>
- a named argument; with
Q> isSumOfMultipliedLeadingEvensEven [2, 4..10]
True
Q> isSumOfMultipliedLeadingEvensEven [2, 4, 5, 6]
True
Q> isSumOfMultipliedLeadingEvensEven []
True
- Define a function that takes the last 3 elements of a
[Int]
and gets the average of them. Use notes as inspiration.
Q> averageOfLast3 [2, 4..10]
8.0
Q> averageOfLast3 [2, 4, 5, 6]
5.0
Q> averageOfLast3 []
0.0
- Define a function using a pipeline that takes a list of strings, takes all the strings that begin with "#", then discards all leading "#" or " " from the resulting strings. Take note of what kind of structure you are working with and what we need to do to work with the structures inside. You may need to start out with an anonymous function[9] somewhere in order to see how you could go from that to a partially applied function.
Q> headingNames ["# Functions", "## Pipelines using partial application", "Random text"]
[ "Functions"
, "Pipelines using partial application"
]
Q> headingNames []
[]
- Remember that
&
is defined inData.Function
and>>>
is defined inControl.Category
. These can be imported withimport Data.Function ((&))
andimport Control.Category ((>>>))
. - sum
- takeWhile
- dropWhile
- isPrefixOf
- any
- elem
- take
- reverse
- Lambdas are written like this:
\argumentOne argumentTwo -> bodyOfFunction
Because we have partial application it's very common to design our APIs in such a way where the "subject" of a function comes last. When we design with partial application in mind we can get functions where the arguments act almost like configuration and we create specialized versions of these on-demand just by partially applying them.
If we take our clamp
function from before as an example:
-- Limits a value to be within the range `lowerBound <= value <= upperBound`
clamp :: Int -> Int -> Int -> Int
clamp lowerBound upperBound value
| value < lowerBound = lowerBound
| value > upperBound = upperBound
| otherwise = value
clampAllToByteRange :: [Int] -> [Int]
clampAllToByteRange = List.map (clamp 0 255)
clampAllToHundreds :: [Int] -> [Int]
clampAllToHundreds = List.map (clamp (-100) 100)
Because List.map
takes the list it is working with as the last argument we can partially apply
List.map
and because clamp
does the same with the value it is clamping we can partially apply
that as well. This leads to very easy code-reuse for different concerns.
If you do happen to mis-design an API with regards to partial application you will definitely feel it. Seeing the issue is trivial and in most cases you'll just swap the order to more effectively make use of partial application. In the absolute worst case you'll simply not use the function with partial application and that's fine too.
It is absolutely worth thinking about this when designing your APIs, but at the end of the day it's not important enough for you to do somersaults in the code base to get it to where it should be.