12

join is defined along with bind to flatten the combined data structure into single structure.

From type system view, (+) 7 :: Num a => a -> a could be considered as a Functor, (+) :: Num a => a -> a -> a could be considered as a Functor of Functor, how to get some intuition about it instead of just relying on type system? Why join (+) 7 === 14?

Even though it is possible to get the final result through manually stepping by the function binding process, it would be great if some intuition were given.

This is from the NICTA exercises.

-- | Binds a function on the reader ((->) t).
--
-- >>> ((*) =<< (+10)) 7
-- 119
instance Bind ((->) t) where
  (=<<) ::
    (a -> ((->) t b))
    -> ((->) t a)
    -> ((->) t b)
  (f =<< a) t =
    f (a t) t

-- | Flattens a combined structure to a single structure.
--
-- >>> join (+) 7
-- 14
join ::
  Bind f =>
  f (f a)
  -> f a
join f =
  id =<< f

*Course.State> :t join (+)
join (+) :: Num a => a -> a
*Course.State> :t join
join :: Bind f => f (f a) -> f a
*Course.State> :t (+)
(+) :: Num a => a -> a -> a
duplode
  • 33,731
  • 7
  • 79
  • 150
Kamel
  • 1,856
  • 1
  • 15
  • 25
  • 8
    “`(+) 7 :: Num a => a -> a` could be considered as a `Functor`...” please don't say stuff like this. `(7+)` is not a functor, it's like saying a carbon atom is a diamond. The functor is `(Int ->)`, i.e. the type constructor that takes types and generates functions from numbers to that type. – leftaroundabout Sep 01 '15 at 08:51
  • For functions, `join f = \a -> f a a`. Thus `join (+) = \a -> a + a`. – AJF Sep 01 '15 at 12:15
  • @leftaroundabout Isn't `(+) 7 :: Num a => a -> a` a functor on category `Num`? – Kamel Sep 01 '15 at 13:29
  • @Kamel: _`Num` as a category_? Phew, let's see. Its objects would be numerical types `n, m, ...`. The morphisms would be functions `n -> m` etc.. So, `(7+)` takes a numerical type and maps it... to itself, fair enough. But in what way does it map morphisms? No, I'm afraid this doesn't make any sense. – leftaroundabout Sep 01 '15 at 17:01
  • @leftaroundabout `(+7)` maps morphisms within numerical type to another morphisms within numerical type, `(+1)` to `(+8)` for example. All functors in Haskell maps category to itself. – Kamel Sep 01 '15 at 20:35
  • 1
    @Kamel: well, you're right, this seems to be in fact a functor, mathematically speaking. (Kinda degenerate one, mind...) Anyway it's not the _function functor_. That doesn't map types to themselves. Regarding “all functors in Haskell maps category to itself” – you mean, they're _endofunctors_ on **Hask**; but that doesn't mean they map objects/types to themselves. In fact that's impossible because functors are type constructors, and fixpoints of those would be “infinite types”. – leftaroundabout Sep 01 '15 at 21:12
  • @leftaroundabout As to *function functor*, do you mean type construction of `:k * -> *`? – Kamel Sep 01 '15 at 22:02
  • @Kamel: `* -> *` is the kind signature of every Haskell functor. By _function functor_, I mean the functors of form `(A -> )`, i.e. the family of functors which are defined by `instance Functor ((->) a)`. – leftaroundabout Sep 02 '15 at 13:39
  • @leftaroundabout Thanks! It is not a Haskell functor, even though it is a functor. – Kamel Sep 03 '15 at 00:46

3 Answers3

12

how to get some intuition about it instead of just relying on type system?

I'd rather say that relying on the type system is a great way to build a specific sort of intuition. The type of join is:

join :: Monad m => m (m a) -> m a

Specialised to (->) r, it becomes:

(r -> (r -> a)) -> (r -> a)

Now let's try to define join for functions:

-- join :: (r -> (r -> a)) -> (r -> a)
join f = -- etc.

We know the result must be a r -> a function:

join f = \x -> -- etc.

However, we do not know anything at all about what the r and a types are, and therefore we know nothing in particular about f :: r -> (r -> a) and x :: r. Our ignorance means there is literally just one thing we can do with them: passing x as an argument, both to f and to f x:

join f = \x -> f x x

Therefore, join for functions passes the same argument twice because that is the only possible implementation. Of course, that implementation is only a proper monadic join because it follows the monad laws:

join . fmap join = join . join
join . fmap return = id
join . return = id

Verifying that might be another nice exercise.

duplode
  • 33,731
  • 7
  • 79
  • 150
7

Going along with the traditional analogy of a monad as a context for computation, join is a method of combining contexts. Let's start with your example. join (+) 7. Using a function as a monad implies the reader monad. (+ 1) is a reader monad which takes the environment and adds one to it. Thus, (+) would be a reader monad within a reader monad. The outer reader monad takes the environment n and returns a reader of the form (n +), which will take a new environment. join simply combines the two environments so that you provide it once and it applies the given parameter twice. join (+) === \x -> (+) x x.

Now, more in general, let's look at some other examples. The Maybe monad represents potential failure. A value of Nothing is a failed computation, whereas a Just x is a success. A Maybe within a Maybe is a computation that could fail twice. A value of Just (Just x) is obviously a success, so joining that produces Just x. A Nothing or a Just Nothing indicates failure at some point, so joining the possible failure should indicate that the computation failed, i.e. Nothing.

A similar analogy can be made for the list monad, for which join is merely concat, the writer monad, which uses the monoidal operator <> to combine the output values in question, or any other monad.

join is a fundamental property of monads and is the operation that makes it significantly stronger than a functor or an applicative functor. Functors can be mapped over, applicatives can be sequences, monads can be combined. Categorically, a monad is often defined as join and return. It just so happens that in Haskell we find it more convenient to define it in terms of return, (>>=), and fmap, but the two definitions have been proven synonymous.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • how is `+ 1` a reader monad? do you mean somehow conceptually or also at the type level? – Erik Kaplun Sep 01 '15 at 13:30
  • 1
    `(+ 1)` is a function that takes an argument and adds one. Conceptually, it can be viewed as a value that depends on a read-only value that it doesn't know yet. `(+ 1)` can be viewed as an integer that we can't examine until we give it the "environment". – Silvio Mayolo Sep 01 '15 at 15:34
  • so it's only a conceptual analogy not a type level fact. – Erik Kaplun Sep 01 '15 at 16:48
  • 1
    @ErikAllik It is more than that. Although `(->) r` is not literally the same thing as `Reader r`, they are isomorphic. Also `(->) r` is [an instance of `MonadReader`](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Reader-Class.html). I'd count that as a "type level fact". – duplode Sep 01 '15 at 17:24
  • 3
    The one point in which this answer could be more precise is in passages like "[using] a function as a monad" and "`(+)` would be a reader monad within a reader monad". Values such as `(+)` are not monads; they are monadic values. It is the type constructors such as `(->) r` that are monads. See also leftaroundabout's comments to the question above. – duplode Sep 01 '15 at 17:27
2

An intuition about join is that is squashes 2 containers into one. .e.g

join [[1]] => [1]
join (Just (Just 1)) => 1
join (a christmas tree decorated with small cristmas tree) => a cristmas tree

etc ...

Now, how can you join functions ? In fact functions, can be seen as a container. If you look at a Hash table for example. You give a key and you get a value (or not). It's a function key -> value (or if you prefer key -> Maybe value). So how would you join 2 HashMap ?

Let's say I have (in python style) h={"a": {"a": 1, "b": 2}, "b" : {"a" : 10, "b" : 20 }} how can I join it, or if you prefer flatten it ? Given "a" which value should I get ? h["a"] gives me {"a":1, "b":2}. The only thing I can do with it is to find "a" again in this new value, which gives me 1. Therefore join h equals to {"a":1, "b":20}.

It's the same for a function.

mb14
  • 22,276
  • 7
  • 60
  • 102