Yeah, agreed – the pure-FP world is different, and you have to be prepared for changes to how your program works – this typically isn’t a topic you pick up fully in a day or two. I agree with the above recommendations, especially to get involved with the Typelevel Discord, which is a pretty friendly and helpful place.
But just to very briefly summarize a few things:
An IO
is essentially a data structure (which contains a lot of code) that describes a program. In a pure-FP application, most of your code is just building that data structure, but not actually doing anything yet. Once you’ve built the entire structure (which is typically an IO
containing more IO
s containing yet more IO
s, often many layers deep), you “run” it, and at that point everything happens.
(Yes, there are serious benefits from this approach, but it takes a bunch of getting used to.)
Very briefly:
map(f)
is the central method of a Functor, and exists on structures that in some sense “contain” things – it means to run the function f
on each value, resulting in a new Functor of the same sort. So myList.map(f)
calls f
on each value in myList
, giving you a new List.
Now, imagine a function listFunc
that itself returns a List[something]
. If you run myList.map(listFunc)
, you’re going to wind up with a List[List[something]]
. That’s typically not what you want – you just want a List[something]
at the end. So List contains a method .flatten
– if you have a List[List[something]]
, then calling .flatten
on it will turn that into a plain List[something]
, by concatenating the internal Lists together.
myList.flatMap(f)
basically does both of those operations – it runs f
on each value in myList
, each of which returns a List
, and then flattens the result. Since you usually want to flatten
after map
, you wind up using flatMap
a lot.
A Monad (very imprecisely speaking) is anything that is shaped like List
– it’s a Functor with a .map()
operation, and there is a reasonable definition of .flatten
, such that calling Monad[Monad[something]].flatten
makes sense. There’s more to it (especially if you want to be precise and correct about it), but in practice that’s usually what you care about in most Scala programming. Lots of Functors are Monads – List
, Option
, Future
, IO
, etc – but not all: it depends on whether there is a sensible meaning for .flatten
.
(This is common enough that Scala supports it, in the form of the for
comprehension. Scala’s for
works on any data type that is Monad-shaped – basically, if it has map
and flatMap
, you can use it in for
. for
is just surface syntax: the compiler turns it into sequences of flatMap
calls, with map
for the last one.)
So if myIO
(which is basically a program) is built from a sequence of IO
s (each a sub-program) inside it, then myIO.flatten
simply means that this program consists of all those sub-program steps, in order. And as a result, an IO
-based application tends to be all about constructing these stacks of nested IO
s, all getting flattened into a single IO
at the top, which you then run.
Hopefully that helps at least a little. Like I said, this stuff takes some getting used to, but I found that, once I really understood it, it made my programming quite a lot more fun…