Fauxnads
So off and on, I've been trying to understand monads. It turns out I have a use case: making web applications in the style I'd like but have them be asynchronous leads to trouble because you need a non-global-variable way of passing along context. I've tried thinking of some solutions, but a friend of mine convinced me that monads, if I could wrap my head around them, would solve the problem for me.
With that in mind, I did some reading... there are plenty of resources out there, some of them with nice pictures, and at one point I tried to understand them by just reading about them and not writing any code relevant to them. This lead to me guessing how they might work by trying to contextualize them to the problems I wanted to solve. So along that path, I had a misunderstanding, a mistaken vision of how monads might work, but while I was wrong, it turned out that this vision of ~monads is kind of fun and had some interesting properties, so I decided to code it up into a paradigm I'm jokingly calling "fauxnads".
Brace yourself, we're about to get into code, pseudo- and real, and it's going to be in Guile. (All code in this blogpost, GPLv3+ or LGPLv3+, as published by the FSF!)
So here's the rundown of fauxnads:
- They still pass along a context, and under the hood, they do pass it in still as the first argument to a function!
- The context gets passed up (and optionally, down... more on that in a few) in some kind of associative array... but we don't want to accidentally change the context that we passed to other functions already, so we'll use something immutable for that.
- The user doesn't really access the context directly. They specify what variables they want out of it, and the fauxnad macro extracts it for them.
- Fauxnads can add properties to the context that they'll call subroutines with so that subsequent fauxnad calls can have access to those.
- Calling child fauxnads happens via invoking a function (=>) exposed to the rest of the fauxnad via some lexical scope hacks.
So when sketching this out, I tried to boil down the idea to a quick demo:
;; fleshed out version of what a fauxnad should approx expand to
(define (context-test1 context arg1 arg2)
(letrec ((new-context context)
;; Define function for manipulating the context
(context-assoc
(lambda (key value)
(set! new-context
(vhash-cons key value new-context))))
;; a kind of apply function
(=>
(lambda (func . args)
(apply func (cons new-context args))))) ;; should be gensym'ed
;; This part would be filled in by the macro.
;; The user would set which variables they want from the context
;; as well as possibly the default values
(let ((a (or (vhash-assoc 'a context) "default-value for a"))
(b (or (vhash-assoc 'b context) "default-value for b"))
(c (or (vhash-assoc 'c context) "default-value for c")))
(values
(begin
(context-assoc 'lol "cats")
(=> context-test2 "sup cat")
(context-assoc 'a "new a")
(=> context-test2 "sup cat")
(format #t "a is ~s, b is ~s, and c is ~s\n"
a b c)
(string-append arg1 " " arg2))
new-context))))
;; intentionally simpler, not a "real fauxnad", to demo
;; the fauxnad concept at its most minimal
(define (context-test2 context arg1)
(begin
(format #t "Got ~s from the context, and ~s from args, mwahaha!\n"
(vhash-assoc 'a context)
arg1))
(values
"yeahhh"
context))
Then calling in the console:
scheme@(guile-user)> (context-test1 (alist->vhash '((a . 1) (b . 2))) "passed arg1" "passed arg2")
Got (a . 1) from the context, and "sup cat" from args, mwahaha!
Got (a . "new a") from the context, and "sup cat" from args, mwahaha!
a is (a . 1), b is (b . 2), and c is "default-value for c"
$79 = "passed arg1 passed arg2"
$80 = #<vhash 2205920 4 pairs>
Okay, whaaa? Let's look at the requirement again. We'll be passing in a function to the start of the function, and then having some other args. We'll then pass that along to subsequent functions. So more or less, we know that looks like this. (I know, not the most useful or pretty or functional code, but it's just a demo of the problem!)
(define (main-request-handler context request)
;; print out hello world in the right language
(display (translate-this (context-get context 'lang) "Hello, world"))
(newline)
;; now call another function
(next-function new-context (smorgify arg1)))
(define (next-function context what-to-smorgify)
(write-to-db
;; Lots of functions need access to the
;; current database connection, so we keep it in the context...
(context-get context 'db-conn)
(smorgify-this what-to-smorgify)))
But wouldn't it be cool if we didn't have to pass around the context? And what if we just said, "we want this and this from the context", then forgot about the rest of the context? We'd never need to call context-get again! It would also be cool to have a way to set things in the context for subsequent calls. Ooh, and if we coud avoid having to type "context" over and over again when passing it into functions, that would also be awesome.
So how about a syntax like this:
(define-fauxnad (our-special-function arg1)
((lang "en")) ;; we want the language variable, but if not set,
;; default to "en" or english
;; (body is below:)
;; print out hello world in the right language
(display (translate-this lang "Hello, world"))
(newline)
;; now call another function
(=> next-function (smorgify arg1)))
We also know we want to use some sort of immutable hashmap. Guile provides vhashes which provide "typically constant-time" data access, and while there are some caveats (a new vhash returned by appending a key/value pair where that key already existed in the vhash will just keep the old pair around... but on our pseudo-stack that shouldn't happen very often, so vhashes should be fine), they work for our purposes.
Okay, cool. So what would that look like, expanded? Something along the lines of:
(define (our-special-function context arg1)
(letrec ((new-context context)
;; Define function for manipulating the context
(context-assoc
(lambda (key value)
(set! new-context
(vhash-cons key value new-context))))
;; a kind of apply function
(=>
(lambda (func . args)
(apply func (cons new-context args))))) ;; should be gensym'ed
;; This part would be filled in by the macro.
;; The user would set which variables they want from the context
;; as well as possibly the default values
(let ((lang (or (vhash-assoc 'lang context) "en")))
(values
(begin
;; print out hello world in the right language
(display (translate-this (context-get context 'lang) "Hello, world"))
(newline)
;; now call another function
(next-function new-context (smorgify arg1)))
new-context))))
As a bonus, we've taken advantage of Guile's multi-value return support, so any parent function which cares can get back the new context we defined for subsequent calls, in case we want to merge contexts or something. But functions not aware of this will simply ignore the second returned parameter. (I'm not sure this is a useful feature or not, but it's nice that Guile makes it easy to implement!)
That's clearly quite a complicated thing to implement manually though... so it's time to write some code to write code. That's right, it's macro time! Guile has some pretty cool hygienic macro support that uses "syntax tranformation"... a bit nicer than common lisp's defmacro, but also less low-level. Anwyay, if you're not familiar with that syntax, trust me that this does the right thing I guess:
(define-syntax define-fauxnad
(lambda (x)
(syntax-case x ()
((_ (func-name . args)
((context-key context-default) ...)
body ...)
(with-syntax ((=> (datum->syntax x '=>))
(context-assoc (datum->syntax x 'context-assoc)))
#'(define (func-name context . args)
(letrec ((new-context context)
;; Define function for manipulating the context
(context-assoc
(lambda (key value)
(set! new-context
(vhash-cons key value new-context))))
;; a kind of apply function
(=>
(lambda (func . func-args)
(apply func (cons new-context func-args))))) ;; should be gensym'ed
;; This part would be filled in by the macro.
;; The user would set which variables they want from the context
;; as well as possibly the default values
(let ((context-key (or (vhash-assoc (quote context-key) context)
context-default))
...)
(values
(begin
body ...)
new-context)))))))))
Nice, now writing our fauxnads is dead-simple:
(define-fauxnad (our-special-function arg1)
((lang "en")) ;; we want the language variable, but if not set,
;; default to "en" or english
;; (body is below:)
;; print out hello world in the right language
(display (translate-this lang "Hello, world"))
(newline)
;; now call another function
(=> next-function (smorgify arg1)))
(define-fauxnad (next-function context what-to-smorgify)
((db-conn #nil))
(write-to-db
;; Lots of functions need access to the
;; current database connection, so we keep it in the context...
(context-get context 'db-conn)
(smorgify-this what-to-smorgify)))
Okay, again, my demos don't make this look very appealing I suppose. We can now transform the original demos I sketched up into fauxnads though:
(define-fauxnad (context-test1 arg1 arg2)
((a "default value for a")
(b "default value for b")
(c "default value for c"))
(context-assoc 'lol "cats")
(=> context-test2 "sup cat")
(context-assoc 'a "new a")
(=> context-test2 "sup cat")
(format #t "a is ~s, b is ~s, and c is ~s\n"
a b c)
(string-append arg1 " " arg2))
(define-fauxnad (context-test2 arg1)
((a #nil))
(format #t "Got ~s from the context, and ~s from args, mwahaha!\n"
a arg1)
"yeahhh")
And calling it:
scheme@(guile-user)> (context-test1 (alist->vhash '((a . 1) (b . 2))) "passed arg1" "passed arg2")
Got (a . 1) from the context, and "sup cat" from args, mwahaha!
Got (a . "new a") from the context, and "sup cat" from args, mwahaha!
a is (a . 1), b is (b . 2), and c is "default value for c"
$81 = "passed arg1 passed arg2"
$82 = #<vhash 2090ae0 4 pairs>
Okay, so what's the point? I doubt this blogpost really would sell anyone on fauxnads, and maybe why would you use fauxnads when you can use real monads? But here's some interesting properties:
- fauxnads are still super simple functions that you can call manually: just pass in the context (a vlist) as the first parameter.
- the "binding" function for calling sub-fauxnads is sugar, but hidden (and otherwise inaccessible, because the function hygeine keeps you from accessing "new-context") from the user.
- I still like that you can get back the new context via multiple value return, but totally ignore it if you don't care about it.
- I understand how they work.
And on that last note, I still don't understand monads, but I feel like I'm getting closer to it. It was fun to document, and put to code, a misunderstanding though!