This is a direct continuation of “Functionally thinking”. If you don’t start a trilogy by the second movie, why should you skip the first article?!
No questionably, you get a lot of good things by thinking functionally. Your entire algorithm becomes a set of functions, beautifully combined to compute results. The programmer tells the computer to do one thing, composing it by using a set of functions. One after another, the software will complete its cycle by outputting the result in a predefined way, like saving a file in the disk, showing something in a screen or performing a request.
Functional programming does not come just with new cool principles from Lambda Calculus and Mathematics. We also need to understand what are the effects caused by giving to functions so much importance and the benefits we get from it. In other words: more new cool principles.
let sumArgs x y = x + y
let times10 x = x * 10
times10 . sumArgs 1 $ 2
I have just one goal here: finish this letting you know why the last line of code above outputs 30 when evaluated by a Haskell compiler and how awesome this is.
Do you remember that languages following principles of Lambda calculus (or “functional languages”) are lazy, right? They won’t process anything until it’s really needed. I mean… REALLY needed. This usually envolve some kind of IO, like showing the result in your screen.
Let’s ask GHCi — our Haskell interactive shell — to apply the function map into a List containing three other Lists:
> map maximum [[1,2,3], [20,10,30], [300,200,100]]
[3,30,300]
For every element of the second parameter ([1,2,3], [20,10,30] and [300,200,100]), map applies its first parameter. And as you might imagine, maximum is a function.
> :t maximum
maximum :: Ord a => [a] -> a
Typing :t <something>
in GHCi, we get the properties of expressions in the language. This expression we call “maximum” takes a List of a’s — call it “anything” — as argument and returns a single element of this same type. Since a is a downcased word, it does not refer to any predefined type. BUT, must implement the Ord class, saying that can be ordered in some way.
But… hey, have you noticed? Come back with me to that map:
map maximum [[1,2,3], [20,10,30], [300,200,100]]
maximum is a function. Something that does a computation based on parameters. And we are using as a parameter to the map function. Not its result after called, but the function itself. This is what in formal ways is called “Higher-order function”: in the most higher level of the language — the same way we use may use the integer 1 — we can use functions. There’s nothing so magical about them, we can pass functions as parameters or even return one in a function.
Talking about functions using functions, we can do this pretty easily.
> let applyWith10 aFunction = aFunction 10
Up there you can see the definition of applyWith10. Applies any function with the integer 10.
> let sumWith2 x = x + 2
> applyWith10 sumWith2 -- has the same result of evaluating `sumWith2 10`
12
Or pass one as parameter to another:
> let complexCalculation = 4 + 8
> sumWith2 complexCalculation -- has the same result of evaluating `sumWith2 4 + 8`
14
Hold yourself, you don’t need to leave your job (already) to start playing with Haskell just because you learned a dozen of new things. Because I have more.
> let sumThreeNumbers x y z = x + y + z
> sumThreeNumbers 2 4 8
14
> let sumTwoNumbersWith2 = sumThreeNumbers 2
> sumTwoNumbersWith2 4 8
14
> let sumNumberWith6 = sumTwoNumbersWith2 4
> sumNumberWith6 8
14
We first define a function receiving 3 parameters. If you apply it with less than the complete number of parameters, Haskell will automatically return a function ready to complete the computation with the remaining parameters.
> :t sumTwoNumbersWith2
sumTwoNumbersWith2 :: Integer -> Integer -> Integer
> :t sumNumberWith6
sumNumberWith6 :: Integer -> Integer
What the type definitions of these intermediate functions mean? sumTwoNumbersWith2
takes 2 integers as parameters and returns another integer. sumNumberWith6
, by itself, takes just 1 integer and returns another. And what do you get from it? Modularity with no sweat. If you think really well on defining interfaces to your functions, like giving good names and receiving nothing different than the needed as parameters, you can share the same function across more blocks of a project, partially applying it and getting just the necessary parts of the algorithm.
You’ve been seeing that I didn’t use parenthesis to apply functions. Neither on their definition.
You can try using them, but they won’t accept being used for this, lowered to a position where they aren’t welcome.
> let sumThreeNumbers(x y z) = x + y + z
:2:21: Parse error in pattern: x
> let sumThreeNumbers (x y z) = x + y + z
:3:22: Parse error in pattern: x
> let sumThreeNumbers x y z = x + y + z
> sumThreeNumbers(2 4 8)
:5:1:
No instance for (Num a0) arising from a use of `sumThreeNumbers'
The type variable `a0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the expression: sumThreeNumbers (2 4 8)
In an equation for `it': it = sumThreeNumbers (2 4 8)
:5:17:
No instance for (Num (a1 -> a2 -> a0)) arising from the literal `2'
Possible fix:
add an instance declaration for (Num (a1 -> a2 -> a0))
In the expression: 2
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
:5:19:
No instance for (Num a1) arising from the literal `4'
The type variable `a1' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the first argument of `2', namely `4'
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
:5:21:
No instance for (Num a2) arising from the literal `8'
The type variable `a2' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the second argument of `2', namely `8'
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
> sumThreeNumbers (2 4 8)
:6:1:
No instance for (Num a0) arising from a use of `sumThreeNumbers'
The type variable `a0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the expression: sumThreeNumbers (2 4 8)
In an equation for `it': it = sumThreeNumbers (2 4 8)
:6:18:
No instance for (Num (a1 -> a2 -> a0)) arising from the literal `2'
Possible fix:
add an instance declaration for (Num (a1 -> a2 -> a0))
In the expression: 2
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
:6:20:
No instance for (Num a1) arising from the literal `4'
The type variable `a1' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the first argument of `2', namely `4'
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
:6:22:
No instance for (Num a2) arising from the literal `8'
The type variable `a2' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Num Double -- Defined in `GHC.Float'
instance Num Float -- Defined in `GHC.Float'
instance Integral a => Num (GHC.Real.Ratio a)
-- Defined in `GHC.Real'
...plus three others
In the second argument of `2', namely `8'
In the first argument of `sumThreeNumbers', namely `(2 4 8)'
In the expression: sumThreeNumbers (2 4 8)
Besides those creepy error messages, we’re already aware about this aspect: we don’t need and won’t be using parenthesis to enclose parameters for a function. Take your time to digest the idea.
How many times have you been in trouble on understanding where is the start/ending of each expression enclosed by parenthesis? What would you do if, from today, you woudn’t need them anymore? Maybe have more time to actually solve problems coding? Ya, that’s sound a good idea for everyone involved in a project.
Let’s think about a use case more practical.
-- Since this block is intended to seem like more real world code,
-- I am using type declaration even when not required.
-- You will need to save it in a *.hs file and load it in a GHCi session
-- by using `:l `
platformFee = 0.03
paymentGatewayFee = 0.01
contributionsAmounts = [1000.0, 2300.0, 100.0, 5100.0, 800.0]
netAmount :: Double -> Double
netAmount grossAmount = (1.0 - platformFee - paymentGatewayFee) * grossAmount
In Neighborly’s context, a crowdfunding platform, we have many contributions to projects. After a campaign is finished, we need to handle the project owner takeout. But first, we need to calculate the net amount of the campaign, subtracting platform (platform owners have the feature of keeping a percentage of received contributions) and payment gateway fees.
At first, we could think of using the above functions in this way:
> netAmount sum contributionsAmounts
:8:1:
Couldn't match expected type `[Integer] -> t0'
with actual type `Double'
The function `netAmount' is applied to two arguments,
but its type `Double -> Double' has only one
In the expression: netAmount sum contributionsAmounts
In an equation for `it': it = netAmount sum contributionsAmounts
:8:11:
Couldn't match expected type `Double' with actual type `[a0] -> a0'
In the first argument of `netAmount', namely `sum'
In the expression: netAmount sum contributionsAmounts
In an equation for `it': it = netAmount sum contributionsAmounts
But, as we already learned, no parenthesis are allowed. This is trying to apply the function netAmount with 2 parameters: the function sum and the list contributionsAmounts, which is wrong.
> let grossAmount = sum contributionsAmounts
> netAmount grossAmount
8928.0
We can give a name to the intermediate step of calculate the gross amont summing all contributions’ amounts. But why, if is going to be referenced just in the following line? We can surely improve this.
> netAmount $ sum contributionsAmounts
8928.0
The only thing special about the $ function is its infix purpose. Unlike functions with alphanumeric characters, $ was made to receive one parameter before its name and another after.
> :t ($)
($) :: (a -> b) -> a -> b
Evaluating netAmount $ sum contributionsAmounts
has almost the same result as netAmount sum
, with the difference that says “parameters after sum
must be passed to sum
function, not netAmount
”. You might have a vague idea why this function is called “function application operator”.
> x 1 2 $ y 3 4 5
The above line will evaluate the expression y 3 4 5
and use the result as third parameter of the x function.
Usually payment gateways asks for whole numbers when dealing with money amounts. The reason is that, if you trigger a transaction with the value 120.222 should he complete with 22 or 23 cents? Or just consider the integral part (120)?
To complete this action, we will need to “compose” a function. In other words, use the result of a function as parameter to another.
> truncate . (* 100) . netAmount $ sum contributionsAmounts
You just used function application to get the sum of all contributions’ amounts, got the net amount from it, composed it using a partially applied function *
, multiplying the number by 100 and truncated it.
> truncate ((netAmount (sum contributionsAmounts)) * 100)
At a higher level, you’re just doing this: applying some functions cleverly.
If this wasn’t already enough to consider Haskell a well designed language, I have more. Much more!