I think https://xkcd.com/483/ should apply to programming languages too. ;-)
But seriously, can someone put this in English? Is it really more complicated than "apply a function to every object in a list"?
I think https://xkcd.com/483/ should apply to programming languages too. ;-)
But seriously, can someone put this in English? Is it really more complicated than "apply a function to every object in a list"?
Most programmers probably know "map" as being an operation takes a function and a list and returns a list of the results of the function applied to each element of the input list. However, the "map" operation can be generalized to generic types other than lists, where it has different behavior but shares certain properties. In functional programming, the idea of a functor gives a name to this pattern.
The idea is that if you have some generic type `T a` covariant in `a`, there is a function `map : (a -> b) -> T a -> T b` that can "lift" every function `f : a -> b` into a function `map f : T a -> T b`. The lifting operation respects function composition and identity.
However, what if the function has multiple arguments, e.g. `f : a -> (b -> c)`? Then, `map f : T a -> T (b -> c)` An applicative functor lets you convert that `T (b -> c)` into a `T b -> T c`. So, with applicative functors, you can individually map each parameter of a multi-parameter function.
A monad is a functor that can be "flattened." It has an operation `join : T (T a) -> T a`, as well as an operation `return : a -> T a`. A monad can be seen as generalizing the idea of flattening a list, or generalizing async/await.
In my day to day I spend a bit of time writing functional code, and a lot of time reviewing it, and when you generalise in this manner it hides a lot of the details of the algorithmic complexity. Is this operation happening in a future? or is on a list? You could argue that the type signature will let you know, but quite often it's inferred.
Suddenly I find myself needing an IDE just to do code reviews. People make arguments about naming variables and suddenly we're back to using hungarian notation.
I also find it's easy to make mistakes - you do a flatmap instead of a map and the Nones just vanish. Or you're composing so many generic functions that the intent of the code just disappears.
I guess i just wanted to see if it's just me.
> Is this operation happening in a future? or is on a list? You could argue that the type signature will let you know, but quite often it's inferred.
1) You are generalizing the input of a function to multiple types. Many of the functions you'd write wouldn't care whether you're using a List or a Future, they would accept either. It's just like declaring your Java method to take a Collection instead of an ArrayList.
2) You are getting more specific in the output of the function. If you write `filter`, it would be nice to get a `LinkedList` back if you pass in a `LinkedList`. With more common type systems, the best you'll be able to do writing the function once is a return type of `Collection` or `Iterable`.
Your method may perform horribly if a LinkedList is passed in instead of an ArrayList. If your operation really depends on a specific behavior the Functor doesn't specify, you should be using a different type than Functor.
I can't imagine a situation where that would be true, except if you're writing a monad library. You could say that you might want to interchange a list of futures for a future of lists, but that's two interdependent things being swapped not one.
For example, there is a useful little function
replicate :: Int -> a -> Seq a
that allows us to build a sequence of n of some value. But what if we don't have a raw value, but instead some Applicative wrapper around it, and instead of just getting a sequence of individually wrapped values, we want that Applicative structure around the sequence? That is, we want some function f that will give us f 3 (Just 1) == Just (fromList [1, 1, 1])
but f 3 Nothing == Nothing
with a Maybe, and f 3 (Right 'x') == Right (fromList "xxx")
but f 3 (Left "error message") == Left "error message"
with an Either, and similarly for any other Applicative.Well, such a function also exists in Data.Sequence:
replicateA :: Applicative f => Int -> f a -> f (Seq a)
Because it works with any Applicative, it can just as well work out all of the possible outcomes when we roll three dice: replicateA 3 [1, 2, 3, 4, 5, 6]
or repeat the action of any Monad three times: replicateA 3 $ putStrLn "Hello, world!"
or repeat whatever behaviour some Applicative you haven't even written yet will have next week.The rabbit hole gets as deep as you like. For example, we can easily compose a variety of other standard functions to count how many ways there are to reach each possible total in that dice example:
map (head &&& length) $ group $ sort $ fmap sum $ replicateA 3 [1, 2, 3, 4, 5, 6]
-- [(3,1),(4,3),(5,6),(6,10), ...]
Many of these functions have quite general types, for example sum working on any Foldable and fmap working on any Functor. Because a list is an Applicative and any Applicative is also a Functor, and since the individual results of the replicateA will be Seqs and any Seq is also a Foldable, everything works fine, and the list structure we started with is preserved all the way through.And then the real kicker is - very often stuff can use that general functor idea ("apply a function to whatever objects are inside this wrapping object, whatever it is") instead of being written against a specific, concrete kind of thing. And then you can use that stuff in all kinds of unexpected places, and combine it with other stuff written in the same way, and it all composes together and works harmoniously.
[1] https://hackage.haskell.org/package/base-4.12.0.0/docs/Prelu...
You'll see the definition and be like, "shit that's just a map." We sell each other a bit short.
Likewise for applicative, monoid, monad -- they're mathematically pretty clear and if you squint while assuming a list is a category, you can probably read most of the definitions and get it.
functor -> equivalent of map for lists
monoid -> equivalent of having an identity ([]) and a join (a.concat) for lists
applicative -> equivalent of turning one element into a list
monad -> equivalent of unwrappering list items, applying a function to each, and rewrapping it
This is just for lists, but the terms are more powerful than that and have many variations. The nice thing is because it's universal. You don't have to wonder, "have I done this pattern right," because you test the laws for them. It might also help, for some of the laws, to remember identity and associativity as like from multiplication -- ie 1 * 9 = 9, 3 * 2 = 2 * 3
This is only one part of the definition of an applicative. Applicatives must also preserve the monoidal operation (product). That is, there is a function `pair : f a -> f b -> f (a * b)`. Then, you can map functions of multiple arguments by turning `(a * b) -> c` into `f (a * b) -> f c` via `map` and passing it the result of the applicative `pair` operation.
> monad -> equivalent of unwrappering list items, applying a function to each, and rewrapping it
I'm not sure how precise or accurate this definition is. I'd say that a monad is about collapsing multiple layers of a functor into one layer.
> This is just for lists, but the terms are more powerful than that and have many variations. The nice thing is because it's universal. You don't have to wonder, "have I done this pattern right," because you test the laws for them. It might also help, for some of the laws, to remember identity and associativity as like from multiplication -- ie 1 * 9 = 9, 3 * 2 = 2 * 3
Yup, I agree that this is a benefit of generalizing these operations based on their common structure.
After reading about monads...honestly no, there is no way to simplify it. Without understanding the type system, it’s alien.
Not more complicated, but yes more general. Perhaps "apply a function to every object in a data structure," even when that data structure can be kind of a abstract.
One example is `Maybe a` which has a value `Just a`. `a` can have any type, including `String`, `Int`, `Float`, etc. But a function that accepts a `String` can't take `Just String` as an argument.
This is the problem that `Functor` solves. It applies your function to the value inside the container.
`Applicative` applies a function that takes two arguments to the values inside two containers. For example, you have the function: `<> :: String -> String -> String` and you apply it to two `Just String` values, like this:
ghci> liftA2 (<>) (Just "hello") (Just " world")
Just "hello world"> Functors: uniform action over a parameterized type, generalizing the map function on lists.
> Is it really more complicated than "apply a function to every object in a list"?
It is defined for more data structures then just a lists. Other than that, not at all.
As far as I can tell, the only reason Haskell has both map and fmap is because "map" came first. When applied to a list, fmap and map are identical.