When learning about Haskell (from the really good book
“Learn You a Haskell for Great Good”
by Miran Lipovača) I came across the four typeclasses from the title - Functor,
Applicative and Monad.
They all seem to be very cool and powerful, but after reading through
all of them it was a bit hard to wrap my mind around it - what is exactly the difference between
them, when should I prefer one over another?
This is why I wrote this small cheat sheet - mostly for myself to better remember and understand it,
but if it helps somebody else as well even better!
So let’s dig in:
Functor
Motivation
If we have map for lists and its obvious how useful it is,
why wouldn’t we have the same thing for any other type of data structure, like e.g. tree?
So, what map is for lists, fmap is for any data structure.
More formally, from Data.Functor Hackage
doc page:
Functors: uniform action over a parametrized type, generalizing the map function on lists.
One-line explanation
Functor is for mapping over parametrized types, such as [], Maybe, trees, …
Typeclass definition
Functor typeclass consists of one function only, fmap. fmap works like this:
input 1: (a -> b) - function that does the mapping (transformation)
input 2: f a - value with a context (“wrapped” value)
output: f b - transformed value with the same type of context
<$> is just the infix version of fmap.
When to use it
When we have a value (e.g. Just 10) or data
structure (e.g. tree) that provides context along with the data, and we want to transform that
data while preserving the type of context.
When not to use it
With functor, we can not change the type of the given context - e.g. after applying
a transformation the list will still be a list, Maybe, will still be a Maybe etc.
Examples
Mapping over a function
This is an interesting example since mapping over a function is a bit less intuitive than mapping
over “container” types such as Maybe or Either.
But, function is nothing else than a (parametrized) type with two parameters, (->) a b, just
like Either a b. So when we map over a function, the result will again be a function. And it works in the exactly the same way, with b being affected. In the context
of a function it can mean only one thing: the mapping function is applied to the result of the
function that is being mapped over, which is a function composition. We can also see in the Prelude source code that is
exactly how the function type implements Functor typeclass:
Let’s take a look at a few specific examples:
(+1) <$> (*3) - just as we mentioned, this will simply compose these two functions. So for arg given, it will return
(arg * 3) + 1.
(+) <$> (*3) - very similar to the previous example, but with a twist that the mapping function takes two arguments, which means it returns a new function when given a single argument.
For arg1 and arg2 given, it will return (arg1 * 3) + arg2.
More example ideas
Show mapping over a function (e.g. parametrized newtype, like Parser in Real World Haskell).
Map function in a context to the value in a context
Can be chained together
We can apply “normal” function to the values with context - “lifting” a function
Motivation
What if we wanted to apply “normal” function to the values with the context? E.g. what if we had
two Maybe values and wanted to use (+) on them? Given Just 3 and Just 5 we’d expect to
receive Just 8 as a result, while given Just 3 and Nothing we’d expect Nothing.
This is exactly what Applicative type class helps us achieve.
There is probably some more motivation but this is what I have for now.
One-line explanation
All Applicative instances must also be Functor instances. Given that, with Applicative we can
apply function in a context to the value in a context.
Typeclass definition
We can see that <*> is very similar to <$>, only that given function also has a context. pure
is a method that enables us to bring some value in a default context (e.g. a “normal” function we
want to use on values with the context).
Examples
Here we can see how we “lifted” a function (+) to work with Maybe values. We have to start with
<$> first since we are starting with a “normal” function without context, so (+) <$> Just 3
produces Just (3+) (or we could have started with pure (+) <*> Just 3).
After that we can just add all the other arguments by chaining them with <*> function.
Monad
Motivation
I am not sure how well did I get the motivation, but here is what I have for now: when dealing
with values with a context, monad is the most general tool for it. When we have values with context
and they are flowing from one method to another, somebody always has to “handle” the context.
In order to avoid doing it manually (which would increase the complexity a lot, and its a
repetitive process),
the monads step in - with them, we can “pretend” we are dealing
with “normal” functions while they appropriately handle the context for us.
One-line explanation
When dealing with values with context, Monad typeclass helps us by automatically handling the
context for us.
Typeclass definition
return is just like pure in Applicative, putting value in a default, minimal context.
(>>=), pronounced “bind”, is the main Monad function. It allows us to “feed” a value with a
context into a function that takes a pure value but returns also a value with a context.
(>>) is used when we want to “execute” one monad before another, without actually caring for its
result (because it probably changes some state, has some side-effect).
fail - apparently never used explicitly in code, but by Haskell for some special cases. Did not
see it in action so far.
Examples
Maybe example -> Show how would handling context look like
without monad
Parsec example
IO example
WIP, ideas
I could write about the situations that drove the inception of the above concepts. E.g.:
First we just had functions that returned a single, simple value.
But soon, we also needed functions that returned multiple different values
(e.g. because they can fail)
The values with context were created to address that need.
But then we needed to handle such types of values throughout the code -> we needed new tools
to help us with that.
SRM 739 was held on October 10th. The original editorial can be found
here. Since I was
participating and I am also currently learning Haskell, I decided to solve all the problems in Haskell
as an exercise and then I found it interesting to compare it with C++ solutions.
Short problem statement: On a straight line cows and barns are located, and each of them is
assigned a position on that line (x coordinate). For each cow determine the barn that is closest
to it. Among two equally close barns choose the one with the lower position.
Due to modest input size limits, here it is more than enough to employ a straightforward
brute-force solution. For each cow we can
examine all the barns and see which one is the closest. If two barns are equally close,
one with the lower position is preferred.
classHungryCowsEasy{public:vector<int>findFood(vector<int>cowPositions,vector<int>barnPositions){vector<int>cowChoice;// Process each cow separately.for(intcowIdx=0;cowIdx<cowPositions.size();cowIdx++){intcowPosition=cowPositions[cowIdx];// Find the nearest barn.intnearestBarnIdx=0;for(intbarnIdx=0;barnIdx<barnPositions.size();barnIdx++){intcurrBestDist=abs(cowPosition-barnPositions[nearestBarnIdx]);intnewDist=abs(cowPosition-barnPositions[barnIdx]);if(newDist<currBestDist){nearestBarnIdx=barnIdx;}elseif(newDist==currBestDist){if(barnPositions[barnIdx]<barnPositions[nearestBarnIdx]){nearestBarnIdx=barnIdx;}}}cowChoice.push_back(nearestBarnIdx);}returncowChoice;}};
Both solutions are short, but I really like how Haskell solution is more readable while being
shorter when compared to C++ solution. In Haskell we could very easily create pairs (idx, value)
and did not have to explicitly handle them as we did in C++.
Short problem statement: We are given two timestamps in hh:mm:ss format, time when the forum
post was published and current time. We have to determine how long ago was a post published and
express it through the appropriate string (e.g. “a few seconds ago”, “X minutes ago”, …).
This problem can actually be solved almost entirely analytically - we just transform both
timestamps to seconds, get the difference and then transform it from seconds back to the
required format (hour,min and sec diff). There is on case to be aware of - if current time
is before the publishing time, it means the post was published the day before.
getShownPostTime::String->String->StringgetShownPostTimecurrentTimeexactPostTime|hourDiff==0&&minDiff==0="few seconds ago"|hourDiff==0=showminDiff++" minutes ago"|otherwise=showhourDiff++" hours ago"where-- Convert string timestamp notations to seconds.currentTimeInSec=hmsToSeccurrentTimeexactPostTimeInSec=hmsToSecexactPostTime-- Calculate the difference in seconds between the currentTime and-- postTime.totalSecDiff-- This is the case where post has been published a day before (E.g.-- if currentTime is "17:23:34" and postTime is "19:02:19"|exactPostTimeInSec>currentTimeInSec=24*3600-absDiff|otherwise=absDiffwhereabsDiff=abs(currentTimeInSec-exactPostTimeInSec)-- Translate difference in seconds to hour diff and min diff.hourDiff=totalSecDiff`div`3600minDiff=(totalSecDiff`mod`3600)`div`60hmsToSec::String->InthmsToSechmsStr=h*3600+m*60+swhere(h:m:s:_)=mapread$splitByColonhmsStrsplitByColon::String->[String]splitByColon[]=[]splitByColon(':':str)=splitByColonstrsplitByColonstr=firstToken:(splitByColonrest)where(firstToken,rest)=break(==':')str
Comparison
Language
Lines
100 runs
C++
43
TBD
Haskell
32
TBD
Not such a huge difference here! What is interesting is that both solutions are actually very
similar, since this problem can be solved in a completely analytical way, we don’t need even one
for loop in the C++ solution (except in the helper method to get time in seconds from the original
format).
It was actually quite straightforward to write a Haskell solution from the C++ solution.
Short problem statement: We are given points respectively describing polygon in a 2D space.
The task is to determine whether a polygon is a regular polygon, which means it satisfies certain
conditions, such as that two adjacent segments are not collinear and none two segments are
intersecting.
If the polygon is regular we should also calculate and return its area.
Input size limits are also not a problem here, so we just need to check if polygon satisfies
all the requirements. This task was mostly about geometry - determining whether two segments
interesect with each other, checking if point lines on a segment, calculating area of the polygon
etc. It was also important to keep all the calculations in integers so we don’t get into precision
problems.
Here are some links I found useful in understanding the formulas employed in the solution:
typedefpair<int,int>Point;#define x first
#define y second
typedefpair<Point,Point>Segment;classCheckPolygon{public:intorientation(Pointa,Pointb,Pointc){longlongo=(longlong)(b.y-a.y)*(c.x-b.x)-(longlong)(c.y-b.y)*(b.x-a.x);if(o>0)return1;if(o<0)return2;return0;}boolareCollinear(Pointa,Pointb,Pointc){// If area of a triangle is 0 -> points are collinearreturnorientation(a,b,c)==0;}boolisPointOnSegment(Pointpoint,Segmentsegment){PointsA=segment.first;PointsB=segment.second;// Must be collinear and within the bounding rectangle of// the segment.returnorientation(point,sA,sB)==0&&(point.x<=max(sA.x,sB.x)&&point.x>=min(sA.x,sB.x))&&(point.y<=max(sA.y,sB.y)&&point.y>=min(sA.y,sB.y));}booldoSegmentsIntersect(Segmenta,Segmentb){// NOTE(matija): what if segments are colinear?returnorientation(a.first,a.second,b.first)!=orientation(a.first,a.second,b.second)&&orientation(b.first,b.second,a.first)!=orientation(b.first,b.second,a.second);}longlonggetPolygonArea(vector<Segment>segments){longlongarea=0;for(inti=0;i<segments.size();i++){PointsA=segments[i].first;PointsB=segments[i].second;area+=(longlong)sA.x*sB.y-(longlong)sB.x*sA.y;}returnarea;}stringcheck(vector<int>X,vector<int>Y){// Create segments of a polygon.vector<pair<Point,Point>>segments;for(intpointIdx=0;pointIdx<X.size();pointIdx++){Pointa={X[pointIdx],Y[pointIdx]};Pointb={X[(pointIdx+1)%X.size()],Y[(pointIdx+1)%X.size()]};segments.push_back({a,b});}// Check every segment.for(intsegmentIdx=0;segmentIdx<segments.size();segmentIdx++){// Has positive length?// Is non-colinear with the next segment?// Does not intersect with other non-neighbouring segments?Segments=segments[segmentIdx];intnextSegmentIdx=(segmentIdx+1)%segments.size();intprevSegmentIdx=(segmentIdx-1)<0?segments.size()-1:segmentIdx-1;if(areCollinear(s.first,s.second,segments[nextSegmentIdx].second)){return"Not simple";}// Check with all non-neighbour segments.for(inti=0;i<segments.size();i++){if(i==segmentIdx||i==nextSegmentIdx||i==prevSegmentIdx)continue;if(doSegmentsIntersect(s,segments[i])){return"Not simple";}}}longlongarea=abs(getPolygonArea(segments));stringstreams;s<<area/2<<'.'<<"05"[area%2];returns.str();}};
typePoint=(Int,Int)typeSegment=(Point,Point)orientation::Point->Point->Point->Intorientation(ax,ay)(bx,by)(cx,cy)|o>0=1|o<0=2|otherwise=0whereo=(by-ay)*(cx-bx)-(cy-by)*(bx-ax)areCollinear::Point->Point->Point->BoolareCollinearabc=(==0)$orientationabcisPointOnSegment::Point->Segment->BoolisPointOnSegmentpoint@(px,py)(sPoint1,sPoint2)=-- Point must be collinear with the segment.orientationpointsPoint1sPoint2==0&&-- Point must be within the bounding box of the segment.px<=maxsaxsbx&&px>=minsaxsbx&&py<=maxsaysby&&py>=minsaysbywhere(sax,say)=sPoint1(sbx,sby)=sPoint2doSegmentsIntersect::Segment->Segment->BooldoSegmentsIntersect(a1,a2)(b1,b2)=orientationa1a2b1/=orientationa1a2b2&&orientationb1b2a1/=orientationb1b2a2getPolygonArea::[Segment]->IntgetPolygonArea=foldraddSegmentToArea0whereaddSegmentToArea((p1x,p1y),(p2x,p2y))area=area+p1x*p2y-p2x*p1ycheckPolygon::[Int]->[Int]->StringcheckPolygonxsys=ifallisGoodsegmentsWithNextAndNonAdjthenareaStrelse"Not simple"whereisGood(s,next,nnss)=not(areCollinearsPoint1sPoint2nextPoint2)&&all(not.doSegmentsIntersects)nnsswhere(sPoint1,sPoint2)=s(_,nextPoint2)=nextsegmentsWithNextAndNonAdj=getNextAndNonAdjacentsegmentssegments=getSegmentsFromPoints$zipxsysareaStr=(show$areaNum`div`2)++decimalPartwhereareaNum=abs$getPolygonAreasegmentsdecimalPart=ifareaNum`mod`2==0then".0"else".5"getSegmentsFromPoints::[Point]->[Segment]getSegmentsFromPointsps=zipWith(\p1p2->(p1,p2))pspsCircL1wherepsCircL1=take(lengthps)$drop1$cyclepsgetNextAndNonAdjacent::[Segment]->[(Segment,Segment,[Segment])]getNextAndNonAdjacentxs=zipWithgetNextAndNonAdjacentForOnexsIndexed(repeatxsIndexed)wherexsIndexed=zip[1..]xsgetNextAndNonAdjacentForOne(idx,x)xsIdx=(x,next,nonAdjElems)wherenext=snd$head$dropidx$concat$repeatxsIdxnonAdjElems=mapsnd$filter(not.areAdjacent(lengthxsIdx)idx.fst)xsIdxareAdjacentlistLengthidx1idx2=idx1==idx2||diff==1||diff==listLength-1wherediff=abs(idx1-idx2)
Comparison
Language
Lines
100 runs
C++
93
TBD
Haskell
68
TBD
Solutions are here again pretty similar! Problem can be nicely decomposed in a functional
way, we need a function for checking each condition, and then we just need to perform checks
for every segment of the polygon.