In the past time I got at multiple occasions into contact with functional development, be it at work, articles, meetups and so forth. But I never really took my time to fully understand the various concepts that are being used.
Now I had some time in my vacation and did a deep dive into the topic and tried to get my head around some concepts with the help of the book Learn You a Haskell for Great Good.
It took me some time to fully understand the topics and I would like to show what helped me understand it.
High-order functions #
Let me first introduce a concept that is crucial for functional development, the high-order functions.
A high-order function differs just slightly from a normal first-order function, it needs to fulfill at least one of the following two requirements:
- Take a function as a parameter
- Return a function
Now, it is best shown with an example.
function add3(x) {
return x + 3
}
function highOrder(operation: Function, value: Number) {
return operation(value)
}
// Calling the high-order function with the two parameters
highOrder(add3, 4)
// returns 7
The highOrder function is being called with the function add3 and the value 4 as a parameter.
Inside, it will then call the add3 function with the value 4.
Type classes #
In the following I will use the type class term, which is being used in the language Haskell.
Now, most likely you have never touched Haskell and you are working with another language that does not use that term.
Giving an example of another language like Java helped me understand the concept a bit further.
I like to imagine a type class is something similar to an interface that extends a class.
The class would be the type and the interface that is extended the class the type class.
Currying #
Another concept of functional development are curried functions.
Currying means that a function that expects multiple parameter can be partially applied with less parameters.
If less parameters are supplied, it will not yet execute the function but return a new function that expects the rest amount of parameters. This goes on until all parameters are filled and the function will be then executed.
Time for an example :)
function add(val1, val2) {
return val1 + val2
}
// Calling the function add with all parameters will execute it and // return the value
add(1,2)
// 3
// With just one parameter, it will return a new function
curriedAdd1 = add(1)
// curriedAdd1 = add(1, val2)
// The now created function curriedAdd1 now expects the last parameter, val2.
// Upon supplying the parameter it will execute the function and return the value
curriedAdd1(3)
// 4
// It is possible to execute the function multiple times, since it is a new function with one variable filled
curriedAdd1(5)
// 6
// Imagine the function curriedAdd1 looks like this:
function curriedAdd1(val2){
return 1 + val2
}
Functor #
The next concept I wanna show is the functor, a type class.
They are also called effect.
A functor type class implements only one function, the map function.
Map #
The map function normally takes two arguments, the function it should apply and the value which will be the parameter to the function.
You might have already heard about map, especially in context with lists.
The list type implements the type class functor which requires the implementation of the map function.
The map function will iterate over the list and apply its given function to each item. At the end it will return a new list with the results of each function call.
Now enough with writing, it is time for an example!
function add3(value){
return value+3
}
[1,2,3].map(add3)
// [4,5,6]
Since the list type has the method map implement, it can be called on the initial list with the three values.
The method takes a function, in this case `add3` as a parameter and it will apply it to each value and return the result.
Now lists are the most famous example of where map is implemented but imagine now other types, for example String, Number or even your own types. It only takes to implement the map function to make it work.
To help the understanding of map and the later methods, I would like to introduce the functional definition of map:
map :: (i -> o) -> e(i) -> e(o)
The definition tells us that the function expects two parameters, the first is a function that transfers the input to the output. The second is the input wrapped in an effect, which I mentioned earlier.
Its output will be the output of the function wrapped in the same effect.
To be a valid map implementation, it need to obey two laws:
Identity law #
This law states that passing a function that just returns the parameter should not lead to any changes.
function id(value){
return value
}
[1,2,3].map(id)
/// [1,2,3]
It needs to leave the original value unchanged.
Composition #
This law states that it should not matter how functions are combined, multiple functions applied to each other in one map or over multiple maps.
function addHello(value){
return value + ‘ Hello’
}
function addWorld(value){
return value + ‘ world!’
}
‘Hi!’.map(addHello).map(addWorld)
‘Hi!’.map(value => addWorld(addHello(value)))
// Hi! Hello world!
Both combinations should return the same string.
Applicative functor #
The applicative functor is a type class that defines two functions, pure and apply.
Its purpose in the world of the functors comes from the limitation of the map method of a normal functor.
The map method expects a function that only allows one parameter.
[1,2,3].map(x => x+3)
// [4,5,6]
Map here can only give one parameter to its function since it iterates always over only one value.
The combination of pure and apply allow to execute a function with more than one parameter.
Pure #
A simple explanation might be that pure lifts a value into a functor.
Now, what kind of functor? Well that depends on the context, later to that more.
Just a small example for now with the list functor.
List.pure(3)
// [3]
function add3(value) {
return value + 3
}
List.pure(add3)
// [add3]
Apply #
Apply is similar to map but it is has a small difference, which is that the supplied function needs to be inside a functor.
Its parameter definition looks like this:
1st parameter: functor with function.
2nd parameter: functor with values.
Now some examples!
I will use here two different notations, one that fits the style I used beforehand and one that fits the description of this function.
One notation defines apply as a standalone function, the other as a function implemented by the type list.
function add3(value) {
return value + 3
}
[1,2,3].apply(List.pure(add3))
List.apply(List.pure(add3), [1,2,3])
// [4,5,6]
// This example is very similar to the map function
function add(val1, val2) {
return val1 + val2
}
[1,2,3].apply(List.pure(add), List.pure(4))
[1,2,3].apply([add], List.pure(4))
[1,2,3].apply([add], [4])
// [5,6,7], all the same result
// Apply will in this case apply the given function add on the first functor, the list [1,2,3]
// This will result in a list of curried add [add(1,val2), add(2,val2), add(3,val2)]
// Apply will then apply for each of those adds the second parameter from the second functor [4]
// The result of that is then a list of the results of those adds, [5,6,7]
[1,2,3].apply([add, add], [4])
// [5,6,7,5,6,7]
[1,2,3].apply(List.pure(add), [1,2])
// [2,3,3,4,4,5]
Now I want you to compare the functional definition of apply with map:
map :: (i -> o ) -> e(i) -> e(o) — Functor
apply :: e(i -> o ) -> e(i) -> e(o) — Applicative
^
Do you see the difference? It is just that with apply, its first argument is wrapped in an effect.
Monoid #
The monoid type class defines two functions, append and concat.
Let me start out with the append function.
Append #
You might not know it but this is one of the functions that you have used all long.
It defines how two instances of the same type should be combined.
This behaviour is, for sure, already implemented for types like lists and strings.
Let me give some examples:
“Hello” + “ World!”
“Hello”.append(“ World!”)
// Hello World!
// Both result in the same output
// Now with lists:
[1,2,3].append([4])
List.append([1,2,3],[4])
// [1,2,3,4]
Concat #
I would describe this function best as resolving nested types of the same type.
Now, this makes most sense for things like lists.
Let me give an example!
List.concat([[1],[2],[3,4]])
// [1,2,3,4]
// The nested lists inside of the outer list are resolved
Monad #
The monad type class extends the applicative functor, it implements just one method, bind.
Bind does multiple actions at the same time.
Bind #
This methods expects two parameters.
1st parameter: Type that implements monad, a list for example.
2nd parameter: A function that returns the same type as given as the first parameter.
Time for an example!
List.bind([1,2,3], (value) => List.pure(value + 3))
List.bind([1,2,3], (value) => [value+3]])
// [4,5,6]
To summarize the differences between the three most important concepts, I will recap the three functional definitions:
map :: (i -> o ) -> e(i) -> e(o) — Functor
apply :: e(i -> o ) -> e(i) -> e(o) — Applicative
bind :: (i -> e(o)) -> e(i) -> e(o) — Monad
^ ^
Please take a look at the differences.
For the bind function, it does not expect to be wrapped in an effect but it expects its output to be wrapped in an effect.
I hope this helped you with understanding the differences between those concepts in a more practical manner.
For further studies, please check out the book Learn You a Haskell for Great Good!, which is available online for free at http://learnyouahaskell.com/ .