;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; The full text of the GNU General Public License can be read at:
;; .
;;----------------------------------------------------------------------
(import (scheme base))
(cond-expand
(guile-3
)
(else
(import
(only (srfi 130) string-split))
))
;; Here is a simple example of a monadic evaluator in Scheme that
;; behaves like any ordinary, non-concurrent, procedural program. It
;; steps through a list of lambdas composing them all together by
;; executing a lambda, and applying the values returned the lambda as
;; the arguments to the next computation:
(define (monad-eval monad . args)
;; Lets say the "monad" arument here is a list of procedures.
(let loop ((procs monad) (args args))
(cond
((null? procs) (apply values args))
(else
(call-with-values
;; first execute the first procedure
(lambda () (apply (car procs) args))
;; then execute the next procedure
(lambda args (loop (cdr procs) args))
)))))
(define monadic-average
(list
(lambda args ;; 1. sum all arguments, return sum and list length
(values (apply + args) (length args)))
(lambda (sum len) ;; 2. display sum, compute and return average
(display "sum = ")(write sum)(newline)
(inexact (/ sum len)))
(lambda (avg) ;; 3. display and return
(display "avg = ")(write avg)(newline)
avg)))
(write
(monad-eval monadic-average -3 -1 4 8 15 16 23 42 66 104))
(newline)
;;--------------------------------------------------------------------------------------------------
;; Our pretty printer may want to parameterize values like
;; indentation, or which output ports to use. Of course, we can use
;; Scheme's built-in parameter objects for this, but lets see how we
;; might construct a monad similar to Haskell's "reader" monad to
;; accomplish this same goal. To construct a "reader" monad, our
;; thunks must always take exactly one argument, the environment
;; argument. The environment will contain the parameters we want to
;; always be available to every thunk in the monad. So lets define two
;; record type: one for our thunks, one for our environment.
;;
;; Also, our monad will have properties of a "writer" monad. This is
;; accomplished by storing a data structure in the monad environment
;; that can collect output. In the case of the pretty printer, this
;; data structure is an output port.
(define-record-type
(%pp thunk) ;; wraps our thunks in a record type
pretty-monad-type?
(thunk pretty-monad-thunk)
)
(define-record-type
(make port indent line)
pretty-env-type?
(port pretty-env-port) ;; port to which we print
(indent pretty-env-indent set!pretty-env-indent) ;; indentation level
(line pretty-env-newline? set!pretty-env-newline) ;; after a line break?
)
(define-record-type
(qstr str)
quoted-string-type?
(str unquote-string)
)
(define (print . args)
;; This is a monad combinator, but it also happens to be the monad's
;; evaluator as well. It can take monads as arguments, and it will
;; recursively calls the evaluator on these arguments, thus defining
;; procedural evaluation semantics. The "pretty" evaluator is just
;; an entry point into this monad.
(%pp ;; Wrap this thunk in a "".
(lambda (env) ;; The thunk takes exactly one argument: a .
(let ((port (pretty-env-port env))) ;; Port is used often, give it a short name
(let loop ((args args)) ;; Loop over the arguments.
(cond
((null? args) (values)) ;; Stop looping here.
((pair? args)
;; First thing: if we are right after a line break, out
;; indentation. This needs to happen inside of this loop
;; since arguments can be other monads which could
;; evaluate the "line-break" monad.
(when (pretty-env-newline? env)
(let loop ((n (pretty-env-indent env)))
(when (> n 0)
(display #\space port)
(loop (- n 1)))
(set!pretty-env-newline env #f)
))
;; Now, inspect the next argument and "display" or "write" it.
(let ((obj (car args))) ;; Get the current argument.
(cond
((string? obj) (display obj port))
((char? obj) (display obj port))
((quoted-string-type? obj)
(write (unquote-string obj) port))
((pretty-monad-type? obj) ;; Recursively evaluate monad arguments
((pretty-monad-thunk obj) env))
(else ;; Use Scheme's built-in writer by default.
(write obj))
))
(loop (cdr args))
)))))))
;; Next, lets define a combinator that outputs a line break only if a
;; line break has not already been output. Our combinator can
;; optionally take a boolean to force the line break regardless. If a
;; line break is written, the "pretty-env-newline?" field of the
;; environment is updated to #t.
(define line-break
(case-lambda
(() (line-break #f))
((forced?)
(%pp ;; wrap this thunk in a ""
(lambda (env) ;; the thunk takes an environment
(unless (and (not forced?) (pretty-env-newline? env))
(newline (pretty-env-port env))
(set!pretty-env-newline env #t))
)))))
;; Finally, let's define a combinator that can recursively evaluate
;; another monad, but with increased indentation. This is an example
;; of how we can simulate the Scheme "parameterize" keyword in our
;; monad.
(define (indent +n monad)
(%pp ;; wrap this thunk
(lambda (env)
(set!pretty-env-indent env (+ (pretty-env-indent env) +n))
((pretty-monad-thunk monad) env)
(set!pretty-env-indent env (- (pretty-env-indent env) +n))
)))
;; Finally, we need our evaluator function, which I will call the
;; "pretty" procedure. The "print" monad is really the evaluator of
;; this monad, so the pretty evaluator is really just an entrypoint
;; into print, we could call "pretty" the top-level evaluator. This
;; top-level evaluator takes any pretty printer monad and applies an
;; environment to the lazy thunk within it to force computation. So we
;; should provide arguments to the evaluator for setting-up the
;; environment.
(define pretty
(case-lambda
((monad) (pretty #t 0 monad))
((init-indent monad) (pretty #t init-indent monad))
((port init-indent monad)
;; We can do some simple runtime type checking
;; when setting up the environment
(cond
((not (integer? init-indent))
(error "non-integer indentation value" init-indent))
((not (pretty-monad-type? monad))
(error "not a pretty-printer monad" monad))
(else (values)))
(let ((port
(cond
((eq? port #f) (open-output-string))
((eq? port #t) (current-output-port))
((output-port? port) port)
(else (error "wrong type for port argument" port)))))
((pretty-monad-thunk monad)
(make port init-indent #t))))
))
;; Now we can declare our monadic computation and evaluate it.
;;
;; Here is a function that produces a list of items
;; that we want to pretty print:
(define (iota+ init n end)
;; Iterate n numbers starting at zero and
;; delimieted by init and end lists.
(define (iota i)
(cond
((< i n) (cons i (iota (+ 1 i))))
((or (pair? end) (null? end)) end)
(else (list end))))
(let loop ((init init))
(cond
((pair? init) (cons (car init) (loop (cdr init))))
((null? init) (iota 0))
(else (cons init (iota 0))
))))
;; This function calls the above "iota+" and
;; pretty prints the result.
(define (print-seq n)
(apply print
(map (lambda (val) (print #\space val)) (iota+ #\( 10 #\)))))
;; Here we pretty print a hello world message with
;; the above list of items in between.
(pretty
(print #\( "hello" (print-seq 10) (line-break)
(indent 4 (print (qstr "world!") #\) ))
(line-break)))
;; Prints:
;; (hello ( 0 1 2 3 4 5 6 7 8 9 )
;: "world!")
;;--------------------------------------------------------------------------------------------------
;; The basic idea of a "list" monad is that you compose a pipeline of
;; procedures, the output of each procedure being applied to the input
;; of each next procedure in the pipieline. However, each procedure
;; has the option of returning more than one result, and when there is
;; more than one result, each result is applied to the next procedure
;; in the chain concurrently.
(define-record-type
;; The wrapper for thunks.
(%list thunk)
list-monad-type?
(thunk list-thunk)
)
;; The combinators
(define (do-list . monads)
;; Each argument to this monad must be a procedure, a lambda, or
;; a list monad that takes one value and returns a list, each value
;; returned is applied concurrently to the procedure that comes
;; after it. All arguments returned are collected into a list.
(%list
(lambda (arg)
(let step-monads ((monads monads) (arg arg))
(cond
((null? monads) arg)
(else
(let*((monad (car monads))
(monad
(cond
((procedure? monad) monad)
((list-monad-type? monad) (list-thunk monad))
(else (error "not a list monad or procedure" monad)))
)
(args (monad arg)) ;; apply the argument to the next monad
)
(apply append
(map (lambda (arg) (step-monads (cdr monads) arg)) args))
)))))))
(define (constants . items)
;; A list monad that simply wraps all of it's arguments, ignoring
;; any input it may receive.
(%list (lambda _ items)))
(define (eval-list-monad init-args . monads)
(let ((monad (list-thunk (apply do-list monads))))
(if (or (pair? init-args) (null? init-args))
(apply append (map monad init-args))
(monad init-args))))
(write
(let ((line-number 0))
(eval-list-monad
(string-append
"As you liberate yourself in metaphor, think of others,\n"
"those who have lost the right to speak.\n"
"As you think of others, think of yourself,\n"
"say: \"if only I were a candle in the dark.\"\n"
)
(lambda (str) (string-split str #\newline))
(lambda (line) (list (string-split line #\space))
;; As an exercise, what happens when we do not wrap
;; the tokenized line in a list?
)
(lambda (line)
(set! line-number (+ 1 line-number))
(list (list (cons line-number line)))
)
)))
(newline)
(define (list-apply elems monad)
(eval-list-monad elems (lambda (elem) (list (monad elem)))))
(write
(list-apply
'(1 2 3)
(lambda (a)
(list-apply
'(1 2 3)
(lambda (b)
(list-apply
'(1 2 3)
(lambda (c) (list (list a b c)))
))))))
(newline)