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 for with:
def withf(context, f):
with context as x:
f(x)
Since this is so trivial, you could not bother to distinguish withf and with.
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 be M a -> (a -> None) -> None (this is actually the type of withf above). Speaking practically, you can use Python’s _ to get a value for the with statement. In Python 3.1:
class DoNothing (object):
def __init__(self, other):
self.other = other
def __enter__(self):
print("enter")
return self.other
def __exit__(self, type, value, traceback):
print("exit %s %s" % (type, value))
with DoNothing([1,2,3]) as l:
len(l)
print(_ + 1)
Since withf uses a function rather than a code block, an alternative to _ is to return the value of the function:
def withf(context, f):
with context as x:
return f(x)
There is another thing preventing with (and withf) from being a monadic bind. The value of the block would have to be a monadic type with the same type constructor as the with item. As it is, with is more generic. Considering agf’s note that every interface is a type constructor, I peg the type of with as M a -> (a -> b) -> b, where M is the context manager interface (the __enter__ and __exit__ methods). In between the types of bind and with is the type M a -> (a -> N b) -> N b. To be a monad, with would have to fail at runtime when b wasn’t M a. Moreover, while you could use with 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 of with (the type is M a -> (a -> b) -> M b):
def withm(context, f):
with context as x:
return type(context)(f(x))
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 as withm) then with could be monadic in the sense of involving monads.