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>


Reply via email to