36

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.

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • 5
    @agf -- it's a yes-or-no question. Well, two yes-or-no questions, but perfectly answerable. – Michael Lorton Aug 20 '11 at 10:04
  • 3
    @agf -- I thought you were the one who voted to close it (and then researched a long disquisition on the subject). I'd ask it on programmers.stackexchange.com but that venue seems to have degenerated into nothing but "My boss is a fascist who makes me write unit tests" and "My subordinate is a hippie who won't write unit tests." – Michael Lorton Aug 20 '11 at 10:30
  • @agf -- are you familiar with the novel *Catch-22* and the character Ex-PFC Wintergreen who "enjoyed working at cross-purposes"? I didn't even realize there *was* a theoretic CS SE site. Thanks. – Michael Lorton Aug 20 '11 at 10:46
  • **ck monads, long live Liskov's rule :D – mlvljr Apr 18 '14 at 18:33

4 Answers4

24

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.

outis
  • 75,655
  • 22
  • 151
  • 221
  • That is a good point about the value of the `with` block; though note there's nothing actually *stopping* it from being another enter/exit sort of thing (sorry I forget the name for those); Python just doesn't enforce types. So I suppose it's a little more general than a monad ;) – Owen Aug 20 '11 at 10:54
  • 1
    The `with` __statement__ produces `l`, the `with` __suite__ produces `_`. – agf Aug 20 '11 at 11:18
  • @Owen: I believe the name for the enter/exit sort of thing is a "context manager", so that's what I'm using. – outis Aug 20 '11 at 11:20
  • @agf: I haven't seen that distinction before, though it makes sense. I see this usage in the ["Compound Statements"](http://docs.python.org/reference/compound_stmts.html) Python documentation. Do you know if it exists outside the Python community? – outis Aug 20 '11 at 11:25
  • @agf: After taking a closer look at the "Compound Statements" page (especially the section on the [with statment](http://docs.python.org/reference/compound_stmts.html#the-with-statement)), I'm going to have to stick with my original wording. What I took you to mean by "statement" is called the "clause header" in the documentation (the `"with" with_item ("," with_item)* ":"` portion). The header and the suite together make the statement; the value of the statement is the value of the suite, which is (as you point out) `_`. – outis Aug 20 '11 at 11:48
  • I may have to go back on my statement that a `with` statement has a value. More properly, it's not the statement itself that has a value but `_` that holds a value. – outis Aug 20 '11 at 11:49
  • I'm not worried about what wording you use, I just wanted to be clear that something is produced by using the `__enter__` method, not just by the suite as a whole. – agf Aug 20 '11 at 11:49
  • I thought of another way to look at "with not producing a value"; which is that a python function that returns nothing is very analogous to `IO ()`; so it could be said to be a value, it's just less flexible. Then `with` "performing" enter+body+exit is analogous to "sequencing" the "value" of the block with the "value" of enter and exit. – Owen Aug 20 '11 at 16:35
  • @Owen: the return type of a function that produces no value in Haskell is `()`. It corresponds to what I (confusingly) originally symbolized with `_`, the void or [bottom type](http://en.wikipedia.org/wiki/Bottom_type) (not the same as [Haskell's bottom](http://www.haskell.org/haskellwiki/Bottom)), in my answer. The exact equivalent to `()` in Python is `None`. ... – outis Aug 20 '11 at 22:38
  • ... `IO ()` is more than a void or unit type. Speaking practically, an `IO ()` can be passed to a function (such as bind) that takes a monad, whereas `()` cannot. For example, using the first `withf`, the expression `withf(withf(DoNothing([1,2,3]), DoNothing), print)` will fail because `withf` has no return value. – outis Aug 20 '11 at 22:40
  • @outis You're right -- the analogy isn't quite perfect. But here's why I think it is imperfect: in Python, the return *type* (type in a Haskell sense) is much more than the `type()` of the return *data*: it should really encompass all side effects that happened during execution. Python doesn't capture those as data because by the time the function returns it's too late. – Owen Aug 20 '11 at 22:56
  • @Owen: that's another good point. A few more points about correctness: `withf(withf(DoNothing([1,2,3]), print), print)` (with the second `withf`) fails because this `withf` doesn't have to return a monad. Replace `withf` with `withm` in either example and the expression will succeed. – outis Aug 20 '11 at 23:03
  • You're comment on types reminds me that there is something slightly more general than a monad which may be applicable here, which allows the types to change in certain ways. Alas I don't know what it's called or how to search for it, but imagine a >>= operator that allows the type constructor to change at each step, but requires to to obey a certain type class. – Owen Aug 20 '11 at 23:53
  • @Owen: [arrows](http://www.haskell.org/arrows/)? Wikipedia has a more extensive article on [arrows in FP](http://en.wikipedia.org/wiki/Arrows_in_functional_programming) than the Haskell site. I'm still only vaguely familiar with them, myself. – outis Aug 21 '11 at 00:06
  • Perhaps. They're too far above my head. – Owen Aug 21 '11 at 00:17
  • @Owen: if there's more, let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/2703/discussion-between-outis-and-owen) – outis Aug 21 '11 at 03:01
12

Yes.

Right below the definition, Wikipedia says:

In object-oriented programming terms, the type construction would correspond to the declaration of the monadic type, the unit function takes the role of a constructor method, and the binding operation contains the logic necessary to execute its registered callbacks (the monadic functions).

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:

Monads, at their most basic level, are more or less a cool way to use continuation-passing style: >>= takes a "producer" and a "callback"; this is also basically what with is: a producer like open(...) and a block of code to be called once it's created.

The full Wikipedia definition:

A type construction that defines, for every underlying type, how to obtain a corresponding monadic type. In Haskell's notation, the name of the monad represents the type constructor. If M is the name of the monad and t is a data type, then "M t" is the corresponding type in the monad.

This sounds like the context manager protocol to me.

A unit function that maps a value in an underlying type to a value in the corresponding monadic type. The result is the "simplest" value in the corresponding type that completely preserves the original value (simplicity being understood appropriately to the monad). In Haskell, this function is called return due to the way it is used in the do-notation described later. The unit function has the polymorphic type t→M t.

The actual implementation of the context manager protocol by the object.

A binding operation of polymorphic type (M t)→(t→M u)→(M u), which Haskell represents by the infix operator >>=. Its first argument is a value in a monadic type, its second argument is a function that maps from the underlying type of the first argument to another monadic type, and its result is in that other monadic type.

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 the with statement was resource management, and a monad is just a useful way to get it.

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
agf
  • 171,228
  • 44
  • 289
  • 238
  • Type construction isn't object construction; the `with` statement is not a type constructor. – Nate Aug 20 '11 at 10:30
  • 1
    No, I think the context manager protocol is essentially a "type construction", the "with statement" is the binding operation. – agf Aug 20 '11 at 10:34
  • 5
    Nice. I'd also like to add that monads, at their most basic level, are more or less a cool way to use continuation-passing style: >>= takes a "producer" and a "callback"; this is also basically what `with` is: a producer like `open(...)` and a block of code to be called once it's created. – Owen Aug 20 '11 at 10:43
  • @Nate -- can you elaborate on what you mean? Isn't the "unit" object construction? – Michael Lorton Aug 20 '11 at 10:47
  • @agf: What type does it construct?? If it made use of reflection somehow to operate on `with`'s parameter, maybe, but `with` just accepts some object that has `__enter__` and `__exit__` methods. – Nate Aug 20 '11 at 10:48
  • @Malvolio - if `with` created some usable type, then it could be called a type constructor, but it doesn't. It doesn't in the same way that `for` doesn't. A type constructor would produce some concrete type, and you would be able to make objects of that type. `with` does not do this. – Nate Aug 20 '11 at 10:51
  • 8
    Python has __duck typing__. Types don't matter -- protocols and interfaces do. So any protocol or interface is essentially a type -- you'll find this philosophy throughout Guido's writings and the Python docs. So the implementation of the __protocol itself__ in Python is a type construction, the implementation of it by the object is a unit function, and the use of it by the `with` statement is a binding operation. – agf Aug 20 '11 at 10:51
  • @agf - then what does it produce with the binding operation? – Nate Aug 20 '11 at 10:57
  • I think it's not too much of a stretch to say it creates a type; You could say `type Guarded a = (IO a, IO ())` where the first is like `__enter__` and produces a result, the second is like `__exit__` and cleans it up. Though see outis's point about how `with` is actually free to return whatever it wants, whereas `>>=` insists on getting a `a -> m b`. But I wouldn't focus too hard on types; Python isn't about types, so it's never going to be pretty. What's interesting about the comparison of `with` to monads is that the style of program flow is very similar. – Owen Aug 20 '11 at 11:09
  • 2
    The insistence that `>>=` take a `a -> M b` is what makes it monadic. If it instead took a `a -> b`, it would be a combinator, which is more general. Monads are all about augmented types. – outis Aug 20 '11 at 21:41
  • Continuation passing style is another name for callbacks, including then from promises/futures, and the goto statement... Inline callbacks are known as blocks in Algol-60, Ruby, and Smalltalk... – aoeu256 Sep 13 '19 at 16:46
9

Haskell has an equivalent of with for files, it's called withFile. This:

with open("file1", "w") as f:
    with open("file2", "r") as g:
        k = g.readline()
        f.write(k)

is equivalent to:

withFile "file1" WriteMode $ \f ->
  withFile "file2" ReadMode $ \g ->
    do k <- hGetLine g
       hPutStr f k

Now, withFile might look like something monadic. Its type is:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

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, a do block without <- arrow).

So I'll answer this question: is withFile monadic?

You could think that it can be written like this:

do f <- withFile "file1" WriteMode
   g <- withFile "file2" ReadMode
   k <- hGetLine g
   hPutStr f k

But this doesn't type check. And it cannot.

It's because in Haskell the IO monad is sequential: if you write

do x <- a
   y <- b
   c

after a is executed, b is executed and then c. There is no "backtrack" to clean a 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.

import System.IO
import Control.Monad.Cont

k :: ContT r IO ()
k = do f <- ContT $ withFile "file1" WriteMode 
       g <- ContT $ withFile "file2" ReadMode 
       lift $ hGetLine g >>= hPutStr f

main = runContT k return

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.

do x <- [2,3,4]
   y <- [0,1]
   return (x+y)

using with (it might be possible with some dirty hacks). Instead, use for:

for x in [2,3,4]:
    for y in [0,1]:
        print x+y

And there's a Haskell function for this - forM:

forM [2,3,4] $ \x ->
  forM [0,1] $ \y ->
    print (x+y)

I recommed reading about yield which bears more resemblance to monads than with: http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html

A related question: if we have monads, do we need exceptions?

Basically no, instead of a function that throws A or returns B you can make a function that returns Either A B. The monad for Either 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.

sdcvvc
  • 25,343
  • 4
  • 66
  • 102
4

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:

class A:

    def __enter__(self):
        print 'Enter A'

    def __exit__(self, *stuff):
        print 'Exit A'

class B:

    def __enter__(self):
        print 'Enter B'

    def __exit__(self, *stuff):
        print 'Exit B'

def foo(a):
    with B() as b:
        print 'Inside'

def bar():
    with A() as a:
        foo(a)

bar()

When run the output is:

Enter A
Enter B
Inside
Exit B
Exit A

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 combine IO (), 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:

data Context a = Context [String] a [String]
    deriving (Show)

a = Context ["Enter A"] () ["Exit A"]
b = Context ["Enter B"] () ["Exit B"]

instance Monad Context where
    return x = Context [] x []
    (Context x1 p y1) >>= f =
        let
            Context x2 q y2 = f p
        in
            Context (x1 ++ x2) q (y2 ++ y1)

foo :: a -> Context String
foo _ = b >> (return "Inside")

bar :: () -> Context String
bar () = a >>= foo

main = do
    print $ bar ()

Which produces:

Context ["Enter A","Enter B"] "Inside" ["Exit B","Exit A"]

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 used String instead of IO actions because it's easier — I think it should be similar with IO actions (correct me if it's not).

And >>= for Context does exactly what with in Python does: it runs the entering statements, feeds the value to the body, 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).

Owen
  • 38,836
  • 14
  • 95
  • 125
  • That's a good [example](http://en.wikipedia.org/wiki/Proof_by_example) of how `with` could be used monadically by translating it into Haskell, but it doesn't prove `with` is monadic. You still need to show the general case, but based on the type problems mentioned in my analysis, I posit it can't be done. Also, it's your definition of `>>=` that preserves sequence; monads don't generally have to preserve sequence (as can be evidenced by replacing `(y2 ++ y1)` with `(y1 ++ y2)`). – outis Aug 20 '11 at 23:14
  • No you're right, you probably can't show a general case, probably because it's not true in general. But it looks to me like the only thing that really stops it from being true in general is that Python let's you "implicitly" turn an `IO ()` into a `Context ()`, which is the type problem you pointed out. If this conversion were made explicit, I think it would be correct. – Owen Aug 20 '11 at 23:17
  • Edited my answer to reflect this. – Owen Aug 20 '11 at 23:20
  • Perhaps we should settle on "It's as monadic as you're gonna get in a language with dynamic duck typing and untracked side-effects" – Owen Aug 20 '11 at 23:23
  • Except that you can get more monadic; `withm`, for example. I suspect that if you extend the context manager interface, you could get a version that tracks side effects (though this is less important, since the main reason Haskell records what in other languages are side effects is that FP doesn't allow for side effects). We could settle on "`with` can be used monadically", which also implies a useful lesson on discipline and practice. – outis Aug 20 '11 at 23:39