Ultimately I don’t think learning them has to be hard, but it can be, and it does usually require a bit of patience to let the full picture sink in.
Ultimately I don’t think learning them has to be hard, but it can be, and it does usually require a bit of patience to let the full picture sink in.
I tried learning about them for fun but couldn't see many examples unless I know all of the weird haskell syntax.
The core basis that I gathered of functors is that they're like anything which is "mappable", the key part being it can be any type, and not something like a list or array. So maybe you have an object data type, and you create a custom function which makes it mappable.
The basis of monads was a functor that returns another functor, and the example I saw was a "stream". Elixir does have streams, and this "makes sense" but I wasn't super satisfied with that answer as I highly doubt all this talk of making it hard to understand is about streams.
Is there an actual example that exists where you can present the different ways to solve it? Ie... you have X data structure, and you want to get Y parts out of it while transforming Z at the same time. Method 1: no monads, Method 2: monads, or something like that?
You could write this without making use of Monads using:
f = case op1 of
Nothing -> Nothing
Just x ->
case op2 x of
Nothing -> Nothing
Just y ->
case op3 y of
...
Or you can use the Maybe monad to make this a bit nicer f = op1 >>= (\x ->
op2 x >>= (\y ->
op3 y >>= (\z -> ...)))
Or you would probably use the do notation, which is just shorthand for >>= f = do x <- op1
y <- op2 x
z <- op3 y
...Let's first think about how we would write that second example in an imperative language (loosely modeled after Java but don't yell at me if it doesn't compile):
Header parseHeader(ByteInputStream bytes) {
int size = bytes.readWord8().toInt();
Word16[] fields = new Array();
for (i : (1...size)) {
fields.add(bytes.readWord16le());
}
return new Header(fields);
}
but that's no good in FP, since we're modifying state on a byte input stream, so let's try to make it more functional (let's make some syntax allowances for that, so it will look less like Java): Header parseHeader(ByteArray bytes) {
val (size, rest) = bytes.nextWord8();
val (_, allFields) = (1...size).fold((rest, [])) { _, (currentRest, fields) ->
val (field, nextRest) = currentRest.nextWord16le();
(nextRest, fields.append(field))
}
return Header(allFields);
}
Now we avoid mutation, so the FP gods are happy, but we have to thread the state through the computation (look at how many variables we used) and it will quickly get out of hand if the structure becomes more complicated. Plus because so many of these variables have the same type, it's easy to accidentally introduce errors.The example with the "Get Header" monad has the advantage of being as simple to read as the imperative example, but purely functional. That works because the Get monad builds up a description of steps to perform, so is purely declarative. It is only executed when I later call it on some actual byte string (e.g. by "runGet parseHeader myBytes"). As an added benefit, this means that I can define the parsing logic without actually calling it. I could also write additional code that takes this representation and does other things on it instead of just running it (e.g. running it with extra logging, or doing some kind of dry-run, etc.)
> That works because the Get monad builds up a description of steps to perform, so is purely declarative.
AWESOME way to put it. Thank you...
I think even experienced people can have issues when a type has a lot of type variables. What do they all mean? It's almost like an implicit API you need to learn for that particilular type, with varying levelsnof documentation or lack thereof