Personally, I think the best way to explain them isn’t to explain what they are, but to explain what they are useful for. You have map that maps (a -> b) functions, and you need another map-like for (a -> m b) functions. 99% of the time, the obvious way to implement that for your structure is the way you’d implement a monad on that structure (plus the ‘default’ a -> m a). That’s why they’re useful. Everything else is burritos.
If I am reading you correctly, all you need to get what you describe is the a -> m a function. Once you have that, it is trivial to go from (a -> b) to (a -> m b) as below:
f :: a -> b
return :: a -> m a
g :: a -> m b
g x = return ( f x )
The critical piece of monads is the ability to turn a generic (a -> m b) function into an (m a -> m b) function.
Essentially, if you have a two argument function whose output is your monadic structure, you can turn it into a function where both arguements are your monadic structure. This is useful because it enable compostability.
Since I'm pretty sure the above would confuse people who are not already familiar with monads, consider nullable types:
f :: a -> Nullable b
g :: b -> Nullable c
Suppose we want to compose these into a new function, h (switching to a more C like notation:
h :: a -> Nullable c
h(x){
maybeY = f(x)
if(maybeY == Null) return Null;
y=maybeY.get();
return g(y);
}
With monads, if you let g' be the "modified" version of g that we discussed above, this just becomes:
h(x) = g'(f(x))
In Haskell, this ' function is called =<<, and has its precedence set such that the above would be written as
h x = g =<< f x
Although you often see it written the other way:
h x = f x >>= g
Or with some nice syntactic sugar:
h x = do
y <- f x
g y
In this last case, you can notice it reads exactly like the C-like code, except there are no explicit null checks.
Another common example is lists. You have functions that take a value and return a list of values:
f :: a -> [b]
g :: b -> [c]
You want to apply g to every element of f and get a 1 dimensional list containing all of the results. This is commonly known as flatMap.
h :: a -> [c]
h(x) = f(x).flatMap(g)
This looks simple, because flatMap is already the monadic bind operator. We just gave it a different name.
My mental model is that applicatives are the multivariate analog of functors.
To invent some terms because I don’t know the real ones, call the types `Maybe Int`, `List Bool`, and the rest of the `m a` types “boxed,” compared to types like Int and Bool which we’ll call “unboxed.”
Functors apply single variable functions that apply purely in the unboxed domain. Take a `List Int` apply an `Int -> Float` function and you get a `List Float`. But you can’t use functors to take a `List Int` and a `List Float` to get a `List Bool` with an `Int -> Float -> Bool` function.
That’s where applicatives come in. Applicatives still operate in terms of applying functions that live in the unboxed domain. `Int -> Float` or `Int -> Float -> Bool` and that kind of thing. But if you had a `List Int` and something boxy like `Int -> List Float`, you have no way to get a `List Float` out the other end.
That’s where monads come in. These can operate on functions that don’t only live in the unboxed domain. You can take your `List Int` and your `Int -> List Float` and get a `List Float` out the other end.
As far as what you need to add for something to be a monad, I think there are cleaner answers that are logically equivalent, but what comes to mind is “flattening” (again for lack of remembering the real terms). Imagine you use the functor or applicative machinery on your boxy functions (that take in unboxed stuff and return boxed stuff). For example, using a `List Int`’s map on a `Int -> List Float`. You end up with `List (List Float)`. To build a monad’s `bind` out of that, you need another function to follow up: `List (List Float) -> List Float`. Aka, flatten that list of lists into a single list.