In functional programming, we have various pillars like Functors, Monads, Applicative and more. Today we will try to explore one of these pillars called Functors. First let’s explore function composition:
- In this diagram, we have 3 sets called A, B, and C.
- Set A contains an element called x.
- Suppose, we need to convert our Set A elements into Set C. But we have no direct path with Set C. Set B is behaving as an intermediator in this case.
- First, we need to compose a function f which converts elements x into result f(x).
- Now, we can see that Set B has element f ( x ), so we need to compose another function g which converts f ( x ) into g ( f ( x ) ).
- By using g ( f ( x ) ), we have successfully converted elements of Set A into Set C.
According to above example, we are creating g ( f ( x ) ) for our results, which is actually called function composition. Let’s take a scala example:
val f = ( x: Int ) => x.String
val g = ( fx: String ) => Symbol( fx )
We are creating two methods. Our requirements are that we need to convert an integer value into the symbol. Let’s suppose that we don’t have any direct way for this conversion. But we have two functions, which will help us to achieve our goal. First, function f which converts Int => String and second, function g, which converts String => Symbol. In that case, we would start composing function like below:
val result = g ( f( x ) )
val result = ( f andThen g )( 8 )
This is called function composition. By using scala-cats, we can compose the functions with map method, which we will be discussing later.
Now, let’s try to figure out about functors.
According to the diagram, we have two sets C and D. But in the case of functors, things are a little bit different.
- In the diagram, we have the following components : (a, b) are elements, F is functor and f is a function.
- The set D is a kind of mirror of set C or we can say, D is used to lift the elements of C with the functors wrapping.
- In functor, the compositions of elements are done within the same set and another set is used as a mirror or a lift for wrapping the elements into functors.
- Rather than composing a function on set C element to set D, we actually lift the elements or in the layman term, by using functors ( F ), we are wrapping the elements into another set.
- The types of elements are independent. Functors can wrap any type of elements.
- In the same set, if an element a is converted into element b as (a => b) with the help of some mapping called f, then in the mirror set, same things will happen, i.e. element Fa => Fb is converted by using Ff mapping.
- In the functors, the method composition is executed in a sequential order.
- In the functors, the structure is always same. For example, while converting Fa => Fb, type of F will always be the same.
Now let’s take a look at some scala examples.
In this diagram, we have a functor called Option. If we wrap or lift the element A and B with functors(like in the above diagram), we are getting Option[A] and Option[B]. As we know, we have mapping method between A => B called f and if we lift the f using functors, in Scala we get an Option of f which is also called a map of f which produces the result Option[A] => Option[B].
def map[A, B](f: A => B): F[A] => F[B]
So, this is the pragmatic signature of the functors. But in Scala, we have a slightly different syntax of functors. Conceptually both have the same meaning, but the below signature is preferred in scala due to method chaining benefit i.e. we can chain multiple methods easily.
def map[A, B]( fa: F[A] )( f: A => B ): F[B] // First Signature
The above syntax of scala map method is available in scala pure functional libraries scala-cats and scalaz. But scala inbuilt functors have a different signature as below :
def map[B] ( f: A => B ): F[B] // Second Signature
This is because, in that case, the map method is created within a functor F[A]. This means that the input for map function is given by F[A] class examples like in scala we have List, Option, and more classes functors. If List or Option contains value(s), it will automatically pass to the map method.
But in the first signature, we will need to pass an object of F[A] within the map method but rest of the behavior is same. This method is useful when we don’t have any idea about the type of functor we will get, but we require to implement A => B on whatever functor is coming.
If we are creating our own functors, then the functor must follow these laws.
- Identity: If we pass an identity function to the functor map method, then the functor must get back the original functor.
- Composition: If two functions execute sequentially like ( f andThen g ) ( x ) or map( f ).map( g ) then the output must be equal to g ( f ( x ) ).
Example 1: Create a custom functor trait which contains a method map and also create an instance of Functor of type List.
Example 2: In Example 1, we are creating our custom Functor trait and providing an implementation for map method, but scala-cats and scalaz provide us with inbuilt Functors and their implementation for predefine type like List, Option and more.