So here's a big HTML file with an SK-combinator graph reduction engine in it. It's 3:00 now, and I started on it around 21:00, so it's taken about 6 hours; this lends some credence to the remark I wrote on FoRK the other day:
[SK-combinators] look like the simplest way to implement a functional language with reasonable efficiency, although anyone who reads kragen-hacks knows that I've implemented several functional languages without reasonable efficiency. I'd go ahead and implement compilation of lambda-expressions into SK-combinators, but I'm too tired now. I'll stick this at http://pobox.com/~kragen/sw/sk.html when I get a chance. I find SK-combinators incredibly hard to program in directly, and nearly impossible to debug. I think I spent 45 minutes trying to debug 'max' --- you know, (lambda (a b) (if (> a b) a b)) in Scheme. On the other hand, I believe this implementation is fully lazy without ever duplicating a reduction, supports currying, does general tail-call elimination, logs all the reduction steps to a log that you can view with "MochiKit.Logging.logger.debuggingBookmarklet()", and even produces a sort of stack trace for any errors. So I think there may be something to be said for this implementation approach. <html><head><title>SK-combinator evaluation by graph reduction</title> <!-- Tested on Firefox 1.5.0.6 with MochiKit 1.3.1. Might work in other browsers. You NEED MochiKit for this. I'm using the following bits of it: $(), map, repr, ifilter, forEach, operator, focusOnLoad, MochiKit.DOM (specifically toHTML, A, and DIV, but it's not very separable), and logging (with log and logDebug - the MochiKit debugging bookmarklet shows reduction steps, very helpful for debugging). In short, this code would be a lot longer without it. Point the next line at MochiKit: --> <script src="Desktop/MochiKit-1.3.1/lib/MochiKit/MochiKit.js"></script> <script> // SK-combinator evaluation by graph reduction // CONSTRUCTORS // Constructor makes an ML-like or Haskell-like constructor (which has // very little in common with an OO constructor, or a JavaScript // constructor). Once you say // App = Constructor("App", ["Fun", "Val"]) // then you can construct an object with {Fun: 3, Val: 4} with App(3, // 4) and you can also set things on App.prototype. You can't new // App(...) though, which you could (and would have to) if App were a // JavaScript constructor. function ConstructorObject(name, argnames, argvals) { this.tagname = name for (var ii = 0; ii < argnames.length; ii++) { this[argnames[ii]] = argvals[ii] } this.argnames = argnames } ConstructorObject.prototype = { toString: function() { var rv = this.tagname for (var ii = 0; ii < this.argnames.length; ii++) { rv = rv + ' ' + this[this.argnames[ii]] } return rv }, repr: function() { var inside = map(repr, this.argvals()).join(', ') return this.tagname + '(' + inside + ')' }, argvals: function() { var self = this return map(function(name) { return self[name] }, self.argnames) } } function ArgumentsMismatch(argvals, argnames) { this.argvals = argvals this.argnames = argnames this.toString = function () { return ('ArgumentsMismatch: got: ' + repr(this.argvals) + ' expected: ' + repr(this.argnames)) } } function Constructor(name, argnames) { var newclass = function() { ConstructorObject.apply(this, arguments) } newclass.prototype = new ConstructorObject(name, argnames, argnames) var rv = function() { if (arguments.length != argnames.length) { throw new ArgumentsMismatch(arguments, argnames) } return new newclass(name, argnames, arguments) } rv.prototype = newclass.prototype // for convenience return rv } // GRAPH NODES // There are three types of graph nodes: applications (App), variables // (Var), and indirection nodes (Ind). An application is a function // being applied to an argument, and a variable is an atom like 'S' or // 'K' or '1' or '+'. An indirection node is merely an alias for some // other node; they exist because we sometimes need to actually mutate // graph nodes into other graph nodes, specifically for K and I. // Originally I had separate constructors for App and Var, but // sometimes in graph-reduction, you need to mutate an App into a Var, // or at least an Ind to a Var. I want different behavior (e.g. for // rendering) in the different kinds of graph nodes, but I don't know // how to change the constructor of a JavaScript object at run-time. // In Python, I would just set the __class__ to change the behavior; // in Perl I would rebless. (The obvious approach, object.constructor // = something_else, doesn't work in my Firefox.) // So now I use a single Node constructor for all three of App, Var, // and Ind, and put the per-type code into strategy objects App_type // etc., and wrote corresponding functions for creation of each node // type. Node = Constructor("Node", ["Type", "Args"]) function delegate(name) { var method = name + '_of' return function() { return this.Type[method].apply(this.Type, this.Args) } } Node.prototype.repr = delegate('repr') Node.prototype.unparenthesized = delegate('unparenthesized') Node.prototype.parenthesized = delegate('parenthesized') Node.prototype.become = function(other) { this.Type = other.Type this.Args = other.Args } Var_type = { tagname: 'Var', repr_of: function(name) { return "Var(" + repr(name) + ")" }, unparenthesized_of: function(name) { return name }, parenthesized_of: function(name) { return name }, } App_type = { tagname: 'App', repr_of: function(fun, arg) { return "App(" + repr(fun) + ", " + repr(arg) + ")" }, unparenthesized_of: function(fun, arg) { return fun.unparenthesized() + ' ' + arg.parenthesized() }, parenthesized_of: function(fun, arg) { return '(' + this.unparenthesized_of(fun, arg) + ')' }, } Ind_type = { tagname: 'Ind', repr_of: function(target) { return "Ind(" + repr(target) + ")" }, unparenthesized_of: function(target) { return target.unparenthesized() }, parenthesized_of: function(target) { return target.parenthesized() }, } function Var(name) { return Node(Var_type, [name]) } function App(fun, arg) { return Node(App_type, [fun, arg]) } function Ind(target) { // no need to create an Ind node for an Ind node if (target.Type == Ind_type) return target else return Node(Ind_type, [target]) } function pretty_print(expr) { return expr.unparenthesized() } // TOKENIZING AND PARSING function tokenize(expr) { // Hooray for regexps! var tokens = expr.match(/\s+|[^\s\(\)]+|\(|\)/g) var wsp_re = /\s/ return ifilter(function(token) { return !token.match(wsp_re) }, tokens) } // parsing exceptions EmptyParens = Constructor("EmptyParens", ["Where"]) ExtraRightParen = Constructor("ExtraRightParen", ["Where"]) MissingRightParen = Constructor("MissingRightParen", []) // wow, this grammar is really easy to shift-reduce by hand :) function sk_parse(expr) { var stack = [null] var tokens_so_far = [] // for error reporting forEach(tokenize(expr), function(token) { tokens_so_far.push(token) var val if (token == '(') { stack.unshift(null) return // do not construct an App } else if (token == ')') { if (stack.length == 1) throw ExtraRightParen(tokens_so_far.join(' ')) val = stack.shift() if (!val) throw EmptyParens(tokens_so_far.join(' ')) } else { val = Var(token) } if (stack[0]) stack[0] = App(stack[0], val) else stack[0] = val }) if (stack.length != 1) throw MissingRightParen() return stack[0] } UnevaluableArgument = Constructor("UnevaluableArgument", ["Fun", "Arg"]) NonNumericArgument = Constructor("NonNumericArgument", ["Fun", "Arg"]) function strict_value(context, expr) { if (expr.Type == Ind_type) expr = expr.Args[0] if (expr.Type != Var_type) expr = sk_reduce(expr) if (expr.Type != Var_type) throw UnevaluableArgument(context, expr.parenthesized()) return expr.Args[0] } function isnan(n) { return n.toString() == 'NaN' } function numeric_value(context, expr) { var string_value = strict_value(context, expr) var value = parseFloat(string_value) if (isnan(value)) throw NonNumericArgument(context, string_value) return value } function binary_arithmetic(name, fun) { return [2, function(a, b) { return Var(fun(numeric_value(name + ' (left)', a), numeric_value(name + ' (right)', b))) }] } InvalidPredicateValue = Constructor("InvalidPredicateValue", ['Value']) built_ins = { // This is the set of combinators from Simon Peyton Jones' 1987 // "The Implementation of Functional Programming Languages", // chapter 16; he says that the B, C, S', and C' combinators were // originated by David Turner in order to implement SASL in the // late 1970s, and the B* combinator by Mark Sheevel of Burroughs // Corp. as an alternative to the B' combinator // B' c f g x -> c f (g x). // S and K are sufficient to build all the others. S: [3, function(f, g, x) { return App(App(f, x), App(g, x)) }], K: [2, function(k, _) { return Ind(k) }], I: [1, function(x) { return Ind(x) }], B: [3, function(f, g, x) { return App(f, App(g, x)) }], C: [3, function(f, g, x) { return App(App(f, x), g) }], "S'": [4, function(c, f, g, x) {return App(App(c, App(f, x)), App(g, x))}], "B*": [4, function(c, f, g, x) {return App(c, App(f, App(g, x)))}], "C'": [4, function(c, f, g, x) {return App(App(c, App(f, x)), g)}], // Again, this can be done with S and K, but it's simpler to: Y: [1, function(f) { return App(f, App(Var("Y"), f)) }], // And here are some arithmetic operators. '+': binary_arithmetic('+', operator.add), '*': binary_arithmetic('*', operator.mul), '/': binary_arithmetic('/', operator.div), '-': binary_arithmetic('-', operator.sub), '%': binary_arithmetic('%', operator.mod), '&': binary_arithmetic('&', operator.and), '|': binary_arithmetic('|', operator.or), '^': binary_arithmetic('^', operator.xor), '<<': binary_arithmetic('<<', operator.lshift), '>>': binary_arithmetic('>>', operator.rshift), '>>>': binary_arithmetic('>>>', operator.zrshift), '==': binary_arithmetic('==', operator.eq), '!=': binary_arithmetic('!=', operator.ne), '>': binary_arithmetic('>', operator.gt), '<': binary_arithmetic('<', operator.lt), '>=': binary_arithmetic('>=', operator.ge), '<=': binary_arithmetic('<=', operator.le), // and a conditional 'if': [3, function(pred, conseq, alt) { var pred_rv = strict_value('if', pred) if (pred_rv == true) return Ind(conseq) else if (pred_rv == false) return Ind(alt) else throw InvalidPredicateValue(pred_rv) }], } // Suppose you want a 'cons' whose result takes two arguments and // applies the first to its contents. That's // \car.\cdr.\f.\g.f car cdr // but translating that by hand into combinators is beyond my ability // at the moment. // Suppose instead we just want 'car'. That's easy --- it's K. How about cdr? // \car.\cdr.cdr // \car.I // K I // Suppose we just want a 'cons' that doesn't afford a nil test: // \car.\cdr.\f.f car cdr // \car.\cdr.C(\f.f car) cdr // \car.\cdr.C (C I car) cdr // \car.B (C (C I car)) I // C (\car.B (C (C I car))) I // C (B B (\car. C (C I car))) I // C (B B (B C (\car. C I car))) I // C (B B (B C (B (C I) I))) I // cons = 'C (B B (B C (B (C I) I))) I' // and that works --- if you apply it to 1 and 2, you get // C (B (C I) I 1) (I 2) // and then applying that to K gives you 1, or applying to (K I) gives you 2. // the 'square' function: // \a.* a a -> S(\a.* a) I -> S(B * I) I // 'max': // \a.\b.if (> a b) a b // \a.S(\b.if (> a b) a) I // \a.S(C (\b.if (> a b)) a) I // \a.S(C (B if (\b. > a b)) a) I // \a.S(C (B if (> a)) a) I // C (\a.S(C (B if (> a)) a)) I // C (B S (\a. C (B if (> a)) a)) I // C (B S (S (\a. C (B if (> a))) I)) I // C (B S (S (B C (\a. B if (> a))) I)) I // C (B S (S (B C (B (B if) >)) I)) I samples = { cons: 'C (B B (B C (B (C I) I))) I', square: 'S (B * I) I', max3: 'S (C (B if (> 3)) 3) I', max: 'C (B S (S (B C (B (B if) >)) I)) I', } function samples_div(input_name) { var rv = [] var inp_code = '$(' + repr(input_name) + ')' for (var name in samples) { var code = inp_code + '.value = ' + repr(samples[name]) + "+ ' '; " + inp_code + '.focus()' rv.push(A({href: 'javascript:' + code + '; void 0'}, name)) rv.push(' ') } return DIV(null, 'Samples: ', rv) } function last(list) { return list[list.length - 1] } ReductionError = Constructor("ReductionError", ["State", "Error"]) ReductionError.prototype.toString = function() { return "ReductionError in " + this.State + " because:\n" + this.Error } function sk_reduce(expr) { var spine = [expr] for (;;) { var expr = spine[spine.length - 1] log("examining " + expr.parenthesized() + " in " + spine[0].parenthesized()) logDebug(' (' + repr(expr) + ')') if (expr.Type == App_type) { spine.push(expr.Args[0]) } else if (expr.Type == Var_type) { var name = expr.Args[0] var defn = built_ins[name] if (!defn) return spine[0] var nargs = defn[0] var fun = defn[1] if (spine.length <= nargs) return spine[0] var args = [] for (var ii = 0; ii < nargs; ii++) { spine.pop() args.push(last(spine).Args[1]) // it's an App, so Args[1] is arg } try { last(spine).become(fun.apply(this, args)) } catch (e) { throw ReductionError(spine[0].parenthesized(), e.toString()) } } else if (expr.Type == Ind_type) { spine.pop() spine.push(expr.Args[0]) } else { throw "up" } } } function eval_sk(expr) { try { return pretty_print(sk_reduce(sk_parse(expr))) } catch (e) { return "Error: " + e } } </script> <script type="text/javascript"> focusOnLoad('input') </script> </head><body> <h1>SK combinators</h1> <form onsubmit="$('results').value += eval_sk($('input').value) + '\n'; return false" > <textarea id="results" cols="80" rows="20"></textarea> <br /> <input id="input" value="S (B * I) I 9" size="80"> </form> <script type="text/javascript"> document.write(toHTML(samples_div('input'))) </script> </body></html>