Like many a foolhardy pioneer before me, I'm endeavoring to cross the trackless wasteland that is Understanding Monads.
I'm still staggering through, but I can't help noticing a certain monad-like quality about Python's with
statement. Consider this fragment:
with open(input_filename, 'r') as f:
for line in f:
process(line)
Consider the open()
call as the "unit" and the block itself as the "bind". The actual monad isn't exposed (uh, unless f
is the monad), but the pattern is there. Isn't it? Or am I just mistaking all of FP for monadry? Or is it just 3 in the morning and anything seems plausible?
A related question: if we have monads, do we need exceptions?
In the above fragment, any failure in the I/O can be hidden from the code. Disk corruption, the absence of the named file, and an empty file can all be treated the same. So no need for a visible IO Exception.
Certainly, Scala's Option
typeclass has eliminated the dreaded Null Pointer Exception
. If you rethought numbers as Monads (with NaN
and DivideByZero
as the special cases)...
Like I said, 3 in the morning.
I have thought unnecessarily long about this and I believe the answer is "yes, when it's used a certain way" (thanks outis :), but not for the reason I thought before.
I mentioned in a comment to agf's answer, that
>>=
is just continuation passing style — give it a producer and a callback and it "runs" the producer and feeds it to the callback. But that's not quite true. Also important is that>>=
has to run some interaction between the producer and the result of the callback.In the case of the List monad, this would be concatenating lists. This interaction is what makes monads special.
But I believe that Python's
with
does do this interaction, just not in the way you might expect.Here's an example python program employing two
with
statements:When run the output is:
Now, Python is an imperative language, so instead of merely producing data, it produces side-effects. But you can think of those side-effects as being data (like
IO ()
) — you can't combine them in all the cool ways you could combineIO ()
, but they're getting at the same goal.So what you should focus on is the sequencing of those operations — that is, the order of the print statements.
Now compare the same program in Haskell:
Which produces:
And the ordering is the same.
The analogy between the two programs is very direct: a
Context
has some "entering" bits, a "body", and some "exiting" bits. I usedString
instead ofIO
actions because it's easier — I think it should be similar withIO
actions (correct me if it's not).And
>>=
forContext
does exactly whatwith
in Python does: it runs the entering statements, feeds the value to thebody
, and runs the exiting statements.(There's another huge difference which is that the body should depend on the entering statements. Again I think that should be fixible).
Yes.
Right below the definition, Wikipedia says:
This sounds to me exactly like the context manager protocol, the implementation of the context manager protocol by the object, and the
with
statement.From @Owen in a comment on this post:
The full Wikipedia definition:
This sounds like the context manager protocol to me.
The actual implementation of the context manager protocol by the object.
This corresponds to the
with
statement and its suite.So yes, I'd say
with
is a monad. I searched PEP 343 and all the related rejected and withdrawn PEPs, and none of them mentioned the word "monad". It certainly applies, but it seems the goal of thewith
statement was resource management, and a monad is just a useful way to get it.Haskell has an equivalent of
with
for files, it's calledwithFile
. This:is equivalent to:
Now,
withFile
might look like something monadic. Its type is:right side looks like
(a -> m b) -> m b
.Another similarity: In Python you can skip
as
, and in Haskell you can use>>
instead of>>=
(or, ado
block without<-
arrow).So I'll answer this question: is
withFile
monadic?You could think that it can be written like this:
But this doesn't type check. And it cannot.
It's because in Haskell the IO monad is sequential: if you write
after
a
is executed,b
is executed and thenc
. There is no "backtrack" to cleana
at the end or something like that.withFile
, on the other hand, has to close the handle after the block is executed.There is another monad, called continuation monad, which allows to do such things. However, you have now two monads, IO and continuations, and using effects of two monads at once requires using monad transformers.
That's ugly. So the answer is: somewhat, but that requires dealing with a lot of subtleties that make the issue rather opaque.
Python's
with
can simulate only a limited bit of what monads can do - add entering and finalization code. I don't think you can simulate e.g.using
with
(it might be possible with some dirty hacks). Instead, use for:And there's a Haskell function for this -
forM
:I recommed reading about
yield
which bears more resemblance to monads thanwith
: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.htmlBasically no, instead of a function that throws A or returns B you can make a function that returns
Either A B
. The monad forEither A
will then behave just like exceptions - if one line of code will return an error, the whole block will.However, that would mean that division would have type
Integer -> Integer -> Either Error Integer
and so on, to catch division by zero. You would have to detect errors (explicitly pattern match or use bind) in any code that uses division or has even slightest possibility of going wrong. Haskell uses exceptions to avoid doing this.It's almost too trivial to mention, but the first problem is that
with
isn't a function and doesn't take a function as an argument. You can easily get around this by writing a function wrapper forwith
:Since this is so trivial, you could not bother to distinguish
withf
andwith
.The second problem with
with
being a monad is that, as a statement rather than an expression, it doesn't have a value. If you could give it a type, it would beM a -> (a -> None) -> None
(this is actually the type ofwithf
above). Speaking practically, you can use Python's_
to get a value for thewith
statement. In Python 3.1:Since
withf
uses a function rather than a code block, an alternative to_
is to return the value of the function:There is another thing preventing
with
(andwithf
) from being a monadic bind. The value of the block would have to be a monadic type with the same type constructor as thewith
item. As it is,with
is more generic. Considering agf's note that every interface is a type constructor, I peg the type ofwith
asM a -> (a -> b) -> b
, where M is the context manager interface (the__enter__
and__exit__
methods). In between the types ofbind
andwith
is the typeM a -> (a -> N b) -> N b
. To be a monad,with
would have to fail at runtime whenb
wasn'tM a
. Moreover, while you could usewith
monadically as a bind operation, it would rarely make sense to do so.The reason you need to make these subtle distinctions is that if you mistakenly consider
with
to be monadic, you'll wind up misusing it and writing programs that will fail due to type errors. In other words, you'll write garbage. What you need to do is distinguish a construct that is a particular thing (e.g. a monad) from one that can be used in the manner of that thing (e.g. again, a monad). The latter requires discipline on the part of a programmer, or the definition of additional constructs to enforce the discipline. Here's a nearly monadic version ofwith
(the type isM a -> (a -> b) -> M b
):In the final analysis, you could consider
with
to be like a combinator, but a more general one than the combinator required by monads (which is bind). There can be more functions using monads than the two required (the list monad also has cons, append and length, for example), so if you defined the appropriate bind operator for context managers (such aswithm
) thenwith
could be monadic in the sense of involving monads.