POV-Ray : Newsgroups : povray.off-topic : I'm in the mood for monads : High theory Server Time
29 Jul 2024 16:29:11 EDT (-0400)
  High theory  
From: Invisible
Date: 23 Apr 2012 06:20:00
Message: <4f952cd0@news.povray.org>
OK, so "magma" consists of a set S and a binary operator #. The # 
operator takes any two elements of S (including the possibility of two 
copies of the same element), and yields a new element of S (possibly the 
same one you started with).

   ∀ x, y ∈ S, x # y ∈ S.

The only "rules" as such are picky, hair-splitting ones. Stuff like "x # 
y" must have a single, definite value for all possible inputs. (If, for 
example, # stands for division, then if y=0, there is no defined result. 
If we're working with integers only, 2/3 isn't an integer. And so on.)

A magma isn't especially interesting by itself. However, if we add the 
rule that # must be /associative/, then we got a "semigroup".

   ∀ x, y, z ∈ S, (x # y) # z = x # (y # z).

Already this begins to have interesting mathematical properties. But if 
we add a special /identity element/, we get a "monoid".

   ∃ i ∈ S: ∀ x ∈ S, i # x = x # i = x.

So what are we really saying? We're saying that a monoid is a set of 
things that have a procedure for combining them, and that there has to 
be a special one where combining it does nothing.

There are many possible examples of this. For example, if S is a set of 
numbers and # denotes addition, then 0 is our identity element. (Every 
child knows that x + 0 = x.) Less obviously, if # denotes 
multiplication, our identity element is 1. (Since 1 * x = x.)

But there are other examples too. If S is the set of all possible text 
strings, and # denotes concatenation, then the identity element is the 
empty string. (Appending or prepending an empty string does nothing.) If 
S is the set of all 1-argument functions [with the same return type as 
their argument type], and # denotes function composition, then the 
identity FUNCTION is the identity ELEMENT. (Bet you can't work out how 
/that/ naming scheme came about. :-P )

If you now take your monoid and give every element a corresponding 
/inverse element/, then you have a "group".

   ∀ x ∈ S, ∃ y ∈ S: x # y = y # x = i.

For example, in the monoid of numbers where # is addition, the inverse 
of (say) 5 would be -5. And hence, numbers and addition actually form a 
/group/, not just a monoid.

Considering strings, however, there is no string that you can append to 
something which "undoes" the act of appending an earlier string. So 
strings with concatenation form a monoid, but not a group.



All of this is fascinating, of course, but not directly related to MONADS.

If somebody asks "what is a monad?", the stereotypical response is

   "A monad is just a monoid in the category of endofunctors - what's 
the problem?"

The problem, obviously, is that if you don't know category theory, this 
makes not one shred of sense.

A less abstract way to think about it is this: If you have a value like 
x, you can put that inside a "box", which I will denote as [x]. Any 
value can be put in a box; in particular, you can put a /box/ in a box, 
for example [[x]].

Every /monad/ is also a /functor/. This merely means that there's a 
function (usually called "map"):

   map :: (x -> y) -> ([x] -> [y])

In other words, it takes a function that works an x-values, and turns it 
into a function that works on boxes containing x-values. (And instead of 
returning y-values, it now returns boxes containing y-values.) In 
summary, it "lifts" a function from working on values, to working on 
values in boxes.

The alternative way to think about map is

   map :: (x -> y) -> [x] -> [y]

In other words, given a function from x to y, and an x in a box, I will 
give you a y in a box.

Either way, there's a way to apply an ordinary function to values in 
boxes. Given that, there are two equivalent ways to define a monad. They 
both involve a function called "return":

   return :: x -> [x]

Return just takes a value and puts it in a box. "Inject" or "insert" 
might be a better name, but for some reason we're stuck with "return". 
Ho hum.

The other function is either "join" or "bind". One can be defined from 
the other - although this is highly non-obvious.

Join is the easy one to understand:

   join :: [[x]] -> [x]

It takes a box within a box, and gives you just a box. It removes one 
level of boxiness. That's pretty easy to comprehend.

Bind, on the other hand, has the hoopy type signature

   bind :: [x] -> (x -> [y]) -> [y]

In other words, given...
- An x in a box.
- A function that wants an x and gives you a y in a box
...I can give you a y in a box.

Notice how the x arrives IN A BOX, but the function expects a value NOT 
IN A BOX. So bind has to take a value /out/ of a box. Right? Right??

Actually... no. You can implement it just using join:

   bind box_x function = join (map function box_x)

Applying the function to the x inside the [x] yields a result of type 
[[y]]. Applying join to that turns it from [[y]] into [y] - the required 
result type.

Less obvious still is that join can be defined using bind:

   join bbx = bind bbx id

Here "id" is the identity function - i.e., a function that takes one 
argument, and just returns it unchanged. Recall that bind takes a value 
out of a box and passes it to a function. Well in this case, the value 
in the box is another box, so if we just /return/ that, we'll have only 
one level of boxing.

Usually it's bind that you actually want to /use/, and hence it's more 
efficient to define join in terms of bind, rather than the other way 
around. (Haskell actually lets you define return and bind without 
bothering with map at all. Map can be defined in terms of bind and 
return, after all: map f = bind (return . f).)



The really hoopy thing, of course, is that this "box" I keep talking 
about usually isn't just a simple box. (If it were, that would give you 
the identity monad, which is the most boring monad there is.) Usually 
it's something more interesting.

For example, if [x] represents a /list/ of x-values (as it does in 
Haskell, by the way), then [[x]] is a list of lists, and join is just 
flattening a list of lists into a single list. Alternatively, bind is 
mapping your function over every element of the list, and then 
flattening the resulting list of lists into a single list. (You will 
sometimes hear Haskell's bind operator is being "concatMap". "concat" is 
a standard library function that does what join does above; "concatMap" 
does the same as calling map and then concat.)

Sometimes, however, Haskellers do crazy things, like where your "box 
with an x in it" is actually a /function/ which takes half a dozen 
arguments and then /returns/ an x. For example, if [x] means "a function 
that takes a dictionary and then returns an x", then... Well, we end up 
with /a lot/ of different functions flying around, which can sound a bit 
confusing. But here it is:

- return(x) just generates a new function that accepts a dictionary, 
ignores it, and always returns x anyway.

- map(f,g) is easy enough. Return a function that takes a dictionary, 
calls g with that dictionary, and then applies f to the result.

- join(f) is taking a function that returns a function that returns an 
x. At this point we don't /have/ a dictionary, so we have nothing to 
actually /call/ f with as an argument. But we want to make a function 
that takes a dictionary and returns an x. So we write a function that 
takes a dictionary argument, calls f with it, and then calls the result 
of f again with the same argument.

- Given that we have join() and map(), it's clear that bind() should be 
implementable. But can we do it more directly? In fact, all bind(f,g) 
does is return a function that takes a dictionary, calls f with that 
argument, and then passes the result to g. Quite easy, really.

As a result of all this jiggery-pokery, you can write simple-looking 
code, but it invisibly has access to this dictionary that we keep 
passing around the place. For that to actually be useful, you need a way 
to /fetch/ the dictionary. You go that by writing a function like

   get_dictionary :: [dictionary]

Notice that this doesn't appear to be a /function/ at all; it takes no 
arguments, after all. But it appears to be a "dictionary in a box" - and 
as we just discussed, in this instance our "box" is really "a function 
that takes a dictionary and returns something". It should be obvious how 
you would write "a function that takes a dictionary and returns a 
dictionary".

So this complex arrangement of stuff allows every piece of code that 
runs inside this specific monad to have [read-only] access to a 
dictionary object. (Obviously it's not hard to change it to be some 
other sort of object instead.) This is the so-called "reader monad".

It's all fascinating stuff... but rather mind-bending in places. ;-)


Post a reply to this message

Copyright 2003-2023 Persistence of Vision Raytracer Pty. Ltd.