简体   繁体   中英

Lambda expressions as monads in Scheme?

I was reading some documents on the web that were trying to present the concept of monad for Scheme programmers. The core idea was that in non-imperative programming monads are used to express the "flow of computation", that is to enforce a sequential evaluation of expressions. Then it struck me: since the body of a lambda expression is evaluated sequentially, should one understand that monads would be superfluous in Scheme? Are lambda bodies evaluated differently in other languages (Haskell, for instance, or ML)?

Sequential Evaluation

So, sequential evaluation of expressions is not characteristic of monads, but of functions . From the moment you write f (gx) (Haskell) or (f (gx)) (Scheme) you have sequenced g as happening before f . In Haskell we instead do this with sequencing operators like (then fg) and (bind f (lambda (x) (make-g x))) but you can do it however you'd like.

The confusion is: monads come with a generic notation in Haskell at least, and this generic notation puts statements in imperative order. From a Scheme perspective this is a macro do which rewrites:

(do
    (put-string "Hey, what's your name?")
    (into x get-line)
    (put-string (string-append "Hi, " x "!")))

to

(then 
    (put-string "Hey, what's your name?")
    (bind get-line (lambda (x) 
        (put-string (string-append "Hi, " x "!")))))

Notice that lambdas get "flattened" and replaced with some new symbol "into". (Actually in Haskell we'd say "from" but it's an infix operator, not a prefix operator.)

What a Monad Is

So what is a monad, really? Well, first off, let's define their part of speech: monads are adjectives , like the adjective "blue". I can have a blue wagon, or a blue wheel. Monads are functors , meaning that given the function from wagons to their wheels, I can give you functions from blue wagons to their blue wheels. So functions can "operate under them". But monads have two special properties. First we have an idea which only applies to "blue" if we make the idea unusually abstract and metaphysical : the adjective must be able to be applied to any value in the language. So you don't just have blue wagons and blue wheels, but also blue functions, blue programs which print "Hello, World!" to the screen, blue everything. Second off the adjective must be condensable in the sense that given a blue blue wagon, you can just give me a blue wagon.

One adjective which does this is "a nullable...". You can always take any value and produce a new, nullable value based on it: just start accepting and detecting null values! If you're in a statically-typed language where things aren't nullable by default, then this is significant. This adjective is a functor: given a function, we just transform null to null and anything else we transform with the function. It is condensable because if we have a "nullable nullable string" that's reducible to a nullable string: take the "null" and "not-null null" values to "null" and the "not-null not-null string" to "not-null string". Finally, to represent any string as a nullable string, convert it to "not-null string". This is called the Maybe monad. It's actually a specialized case of the Either y monad, which could hold a y .

Another adjective which does this is "a list of...". To apply a function under the adjective, apply it to all members of the list. To condense a list-of-lists into a list, concatenate its elements. To create a list of x 's from any x , give me back the one-element list. This is called the "list monad". Writing this way is a lot like writing list comprehensions, and in fact is isomorphic to Clojure transducers, so you get maps & filters for free.

Another adjective which does this is "a function from s to an s and a...". To create one of these from any x , just pair the incoming state with the x . To apply a function, just apply it to the appropriate side of the output pair. Easy peasy. Finally, if you have the convoluted s -> (s, s -> (s, a)) situation, construct an s -> (s, a) by feeding the incoming s to the convoluted function, taking the s that comes out of that and feeding it to the s -> (s, a) which comes out of it. This gives you the (s, a) that you need to return. This is called the " State s monad" because you can envision the "s" as a typed state.

Another adjective which does this is "a program which returns a". If you have an int, we can construct a program which does nothing and returns that int. If you have a program which returns an int and a function from ints to something else, we can do the program, then apply the function to the output of the program. Finally, if you have "a program which returns a program which returns an int," just form a program which runs the outer program to compute the inner one, and then runs the inner one. This is called the IO x monad. (Similarly with promises: a promised promised x is not much different from a promised x.)

You can see that it's a very broad pattern, and it doesn't always clearly involve "sequence".

Comparing Scheme to Haskell

Once you understand that Haskell describes all its I/O operations by containing programs as values and then combining them together, you realize that basically we do functional I/O by becoming a macro language . The macro language doesn't do any I/O by itself, but you use it to construct programs which do your I/O for you. You feed those into the compiler, and the compiler produces the actual program which does what you want it to do. Interpreters get a little more dicey because the interpreter needs to say "if I see a value which is not a program, I will try to print that; if I see a value which is a program, I will try to run that program and then print that." This means that the syntax at the command prompt is subtly different from the syntax in a file.

Once you understand that Haskell is a metaprogramming environment where we're constructing a program-as-a-value, you will understand how Haskell does functional I/O and you'll see the programmable syntax for "monads" as a meta-metaprogramming design. This design is much less complicated than macros (which we also have in Haskell; it's called "Template Haskell") but gets the job done for a lot of useful cases. And it's essentially about overloading, at some level, what "a; b; c; d" means to be dependent on the (common) outermost adjective of the types of a, b, c, and d. For I/O it means "do a, then do b, then..."; for states it means "thread the state produced by a into b, then the state produced by b into c, then..."; for nullables it means "do a, if that's not null use it to do b, if that's not null use it to do c...". For lists it's a Cartesian product of the constituent lists, "pair up everything in all the ways."

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM