0 valutazioniIl 0% ha trovato utile questo documento (0 voti)

3 visualizzazioni5 paginemond for schemers

monad for schemers

© © All Rights Reserved

PDF, TXT o leggi online da Scribd

mond for schemers

© All Rights Reserved

0 valutazioniIl 0% ha trovato utile questo documento (0 voti)

3 visualizzazioni5 paginemonad for schemers

mond for schemers

© All Rights Reserved

Sei sulla pagina 1di 5

txt

Dave Herman

Draft of 13 July 2004

I. Introduction.

with the assumption that the reader is comfortable with continuations,

CPS, accumulators, and accumulator-passing style.

The main insight of monads is that all side effects, from mutation to

I/O to non-termination, have one thing in common: order of evaluation

matters. In simple, terminating, pure lambda expressions, the order of

evaluation is completely irrelevant: no matter how you reduce it, the

final result is the same with no observable differences. But when you

have side effects, they have to happen in the right order. (Monads

aren't the only formalism for dealing with this -- CPS and A-normal

form do, too. But they're all related.)

semantics. Consider trying to implement the following impure program

in a pure subset of Scheme:

(begin (turn-on-safety!)

(pull-trigger!))

arguments (we'll just stick to a two-argument definition) and evaluate

to the value of the second. With the naive definition:

(begin (turn-on-safety!)

(pull-trigger!))

-> (begin (turn-on-safety!) ; effect: pull-trigger!

#<void>)

-> (begin #<void> ; effect: turn-on-safety!

#<void>)

-> #<void>

committee members. Whoops. Let's CPS our programs so we can enforce

the evaluation order (I'm using the infix [[--]] to represent the

CPS-translation of an expression):

[[(begin (turn-on-safety!)

(pull-trigger!))]]

= (lambda (k)

([[turn-on-safety!]] (lambda (res1)

([[pull-trigger!]] (lambda (res2)

(k res2))))))

(lambda (k)

(cps-exp1 (lambda (res1)

(cps-exp2 (lambda (res2)

(k res2)))))))

argument res1, and subsequently ignored (no subexpression refers to

res1).

http://www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt 1/5

10/10/2018 www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt

where all procedures have to take an extra argument that represents a

"register" that's being updated as a computation proceeds. Consider a

little random-number generator:

(define (rand)

(let ([ans (modulo (* seed 16807) 2147483647)])

(begin (set! seed ans)

ans)))

generated random number. If we were to implement this in pure Scheme,

we'd need to pass around the seed as an extra argument through any

procedures that might update its value. We'd also have all our

procedures return a pair of values: the actual result of the

procedure, plus the new seed, in case it got updated during the

computation of the procedure's result.

(define (rand seed)

(let ([ans (modulo (* seed 16807) 2147483647)])

(cons ans ans)))

(define (rand-point seed)

(let* ([r1 (rand seed)]

[r2 (rand (cdr r1))]

[r3 (rand (cdr r2))])

(cons (make-point (car r1) (car r2) (car r3))

(cdr r3))))

(define (rand-segment seed)

(let* ([r1 (rand-point seed)]

[r2 (rand-point (cdr r1))])

(cons (make-segment (car r1) (car r2))

(cdr r2))))

...

The whole program would start with an initial seed like so:

(run-my-program (current-time))

that they take a "seed" parameter and return a pair consisting of

their result and the new seed. Let's start abstracting that out by

currying the seed parameter:

(define (rand)

(lambda (seed)

(let ([ans (modulo (* seed 16807) 2147483647)])

(cons ans ans))))

(define (rand-point)

(lambda (seed)

(let* ([r1 ((rand) seed)]

[r2 ((rand) (cdr r1))]

[r3 ((rand) (cdr r2))])

(cons (make-point (car r1) (car r2))

(cdr r2)))))

(define (rand-segment)

(lambda (seed)

http://www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt 2/5

10/10/2018 www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt

(let* ([r1 ((rand-point) seed)]

[r2 ((rand-point) (cdr r1))])

(cons (make-segment (car r1) (car r2))

(cdr r2)))))

Any procedure that can't see or change the value of the seed can be

left unchanged. We call the procedures that can have side effects

"operations," and procedures that can't "pure." For example, we could

write a distance function:

(sqrt (+ (sqr (- (point-x pt1) (point-x pt2)))

(sqr (- (point-y pt1) (point-y pt2)))

(sqr (- (point-z pt1) (point-z pt2))))))

take an extra parameter to represent the current seed, and it doesn't

return a pair. This means that any possibly-effectful procedure can

call distance, but distance can't call any possibly-effectful

procedure (since it would disregard the effect and wouldn't pass it on

to the next operation).

operations:

(define (get-seed)

(lambda (seed)

(cons seed seed)))

(define (set-seed new)

(lambda (old)

(cons (void) new)))

We can abstract the common pattern in the types, too: we say that an

operation that returns a value of type alpha has type T(alpha), where:

set-seed : number -> T(void)

rand : -> T(number)

rand-point : -> T(point)

rand-segment : -> T(segment)

Next we try to define BEGIN like before, only instead of in CPS, it's

just threading the accumulator through:

(define (begin comp1 comp2)

(lambda (seed0)

(let* ([res1 (comp1 seed0)]

[val1 (car res1)]

[seed1 (cdr res1)])

(comp2 seed1))))

two operations and returns a new operation. This isn't quite useful

for implementing our operations, though:

(define (rand)

(begin (get-seed)

(let ([ans (modulo (* ??? 16807) 2147483647)])

(begin (set-seed ans)

(lambda (seed)

(cons ans ans))))))

http://www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt 3/5

10/10/2018 www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt

How does the second operation in RAND get the current seed from the

GET-SEED operation? The problem is that BEGIN disregards the result of

the first operation. Let's write a new combinator that makes the

result of the first operation available to the second:

(define (pipe comp1 build-comp2)

(lambda (seed0)

(let* ([res1 (comp1 seed0)]

[val1 (car res1)]

[seed1 (cdr res1)])

((build-comp2 val1) seed1))))

the result of the first operation and constructs a second operation

based on the result of the first. Finally, it runs the second

operation.

(define (rand)

(pipe (get-seed)

(lambda (seed)

(let ([ans (modulo (* seed 16807) 2147483647)])

(begin (set-seed ans)

(lambda (seed)

(cons ans ans)))))))

operation:

(define (lift v)

(lambda (seed)

(cons v seed)))

(define (rand)

(pipe (get-seed)

(lambda (seed)

(let ([ans (modulo (* seed 16807) 2147483647)])

(begin (set-seed ans)

(lift ans))))))

"operations" as well as two operations:

pipe : T(alpha) (alpha -> T(beta)) -> T(beta)

>>=, *, or let. The lift operation is often called unit or return.]

(pipe (lift x) f) = (f x)

(pipe m lift) = m

(pipe (pipe m f) g) = (pipe m (lambda (x) (pipe (f x) g)))

[I'm tired and I don't feel like proving the monad laws hold for our

example. Actually, they probably don't because our version of pipe has

two arguments (instead of being completely curried) -- non-termination

would screw this up. Oh well.]

The monad can have any other operations as long as they don't

invalidate the laws.

needs an initial seed in order to produce a value. Also, notice that

http://www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt 4/5

10/10/2018 www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt

the result of running an operation produces a pair of values

containing the final value plus the accumulated seed. Since we're

really just interested in the final value, we can construct a "run"

procedure to execute a monadic operation and extract its final value:

(define (run m)

(car (m (current-time))))

Notice also that this is the only way to "get out" of the monad, i.e.,

to go from T(alpha) to alpha. A monadic operation is built up with

combinators to produce one long chain of operations and then "run"

with a top-level procedure. This top-level procedure is very similar

to the process of running a CPS-ed program with an initial

continuation.

IV. Summary.

2. abstracting away accumulators

from two smaller operations; it forces the operations to be performed

in order in much the same way as CPS. (In fact, it turns out that CPS

is a special case of a monad.)

effects will be guaranteed to happen in the right order. This is

useful for programming language semantics, because it lets us model

useful language features like mutable state and first-class

continuations in a pure functional semantics (i.e., lambda-calculus),

and it lets us reason abstractly about sequential execution of

effects.

a lazy language. In particular they use monads to perform I/O so that

all I/O operations are guaranteed to happen in the right order. They

also use monads to simulate various kinds of effects that they decided

to leave out of the base language (e.g., mutable state and first-class

continuations).

V. More.

use of the algebraic laws, nor the math behind them. In fact, I

haven't even defined monads. The reason is that a monad is really a

semantic object, which is hard to point to when you're looking at raw

code. So I've decided to stick just to the intuitions and leave the

precise definitions for another day. So there's lots more to learn.

http://www.ccs.neu.edu/home/dherman/research/tutorials/monads-for-schemers.txt 5/5