4 ms·
It's not even about the effects, really. It's simpler to think of Monad as an interface for manipulating encapsulated data in a way which keeps it encapsulated.
by chriswarbo 12y ago
It's not even about the effects, really. It's simpler to think of Monad as an interface for manipulating encapsulated data in a way which keeps it encapsulated.
The simplest way to encapsulate data is to wrap it using a constructor which we keep private to our module:
module M1 (Encap(), val1, val2)
data Encap a = mkEncap a
val1 :: Encap Int
val1 = mkEncap 5
val2 :: Encap Int
val2 = mkEncap 10
Other modules importing M1 get access to the Encap type, val1 and val2 but not the mkEncap constructor. They can use val1 and val2 as-is, but they can't construct new Encap values or destruct existing Encap values to get at their contents. The problem is, there's not much we can do with this interface.
One way we can make this more useful is being able to apply some function to an encapsulated value. That's what Functor is for, so we can add this to M1:
instance Functor Encap where
fmap f (mkEncap x) = mkEncap (f x)
Now users of M1 can transform encapsulated values without being able to break the encapsulation. For example:
val1Plus7 :: Encap Int
val1Plus7 = fmap (+7) val1
val2Str :: Encap String
val2Str = fmap show val2
We're still pretty limited though, since there's no way to combine encapsulated values into new encapsulated values, or to encapsulate our own values. That's what Applicative provides, by letting us construct encapsulated values without gaining the ability to destruct them, and by allowing encapsulated functions to be applied to encapsulated values (since functions are closures, this lets us gather up encapsulated values and combine them arbitrarily):
instance Applicative Encap where
pure x = mkEncap x
(mkEncap f) <*> (mkEncap x) = mkEncap (f x)
Now users of M1 can encapsulate and combine values, like this:
-- val1 + val2
val1PlusVal2 :: Encap Int
val1PlusVal2 = fmap (+) val1 <*> val2
-- New encapsulated string
val3 :: Encap String
val3 = pure "Hello world"
-- val3 repeated val1 times
val3Repeated :: Encap String
val3Repeated = fmap rep val1 <*> val3
where rep n _ | n <= 0 = ""
rep n s = s ++ rep (n-1) s
This is quite a powerful interface, but one thing we can't do is `collapse` double-encapsulated values into single-encapsulated values. That's what Monad provides:
join :: Encap (Encap a) -> Encap a
join (mkEncap (mkEncap x)) = mkEncap x
An alternative, but equivalent, definition is to allow calls to encapsulation-producing functions without encapsulating their result. Haskell's Monad is defined this way:
instance Monad Encap where
(>>=) :: Encap a -> (a -> Encap b) -> Encap b
(mkEncap x) >>= f = f x
These kind of encapsulated values turns out to hold effects without breaking the language, and these interfaces turn out to be powerful enough for general computation.
- agumonkey 12y agoI like to rename encapsulation as invariant these days. List monad will ensure you have a list of a.
- physPop 12y agoThis is a wonderful explanation, thank you!
- Flow 12y agoI agree. I read that comment last night and could almost not sleep due to the insights it gave me. I'm going to spend the whole weekend re-reading about Functors, Applicative and Monads now. Maybe this time I'll grok it all. Thanks Chris!
- jberryman 12y ago>These kind of encapsulated values turns out to hold effects without breaking the language I wonder if you're hand-waving a bit here. To me "encapsulated values" describes Identity or Maybe, but really doesn't work (IMO) for e.g. State or IO where bind is composition.
- chriswarbo 12y agoI would use the term "wrapped-up" where you've used "encapsulated", and in fact I specifically avoided talking about wrappers for this exact reason :) Maybe my terminology could have been better, but I meant "encapsulated" in analogy to OOP, which advocates "encapsulating" all data via methods. The OOP definition of encapsulation includes using getters/setters, which is like having "wrapped-up" properties, but the idea is that we can go beyond this to calculate the data in arbitrary ways without our clients having to know about the implementation. That's what I was trying to get at here; for example, the implementation of IO involves horrible imperative yukiness, but we (the client) don't need to know that: we just use the interface, and if our functions ever get called, they will be given an appropriate argument.