From 67720f4f6aa4267cf79a3e1f94ce40c164b5a935 Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Wed, 13 May 2015 16:20:16 -0700 Subject: [PATCH] Clean up and initial commit. --- .gitignore | 18 +++++++++++ LICENSE | 31 ++++++++++++++++++ Makefile | 16 +++++++++ README.md | 32 ++++++++++++++++++ bin/activate | 8 +++++ package.json | 29 +++++++++++++++++ src/lists.coffee | 81 ++++++++++++++++++++++++++++++++++++++++++++++ src/reduce.coffee | 60 ++++++++++++++++++++++++++++++++++ test/lists.coffee | 75 ++++++++++++++++++++++++++++++++++++++++++ test/reduce.coffee | 56 ++++++++++++++++++++++++++++++++ 10 files changed, 406 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/activate create mode 100644 package.json create mode 100644 src/lists.coffee create mode 100644 src/reduce.coffee create mode 100644 test/lists.coffee create mode 100644 test/reduce.coffee diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d1d525 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*# +.#* +*~ +*.orig +npm-debug.log +node_modules/* +bootstrap/*.js +notes/ +tmp/ +.tup +package.yml +bootstrap/crisp.js +bin/cake +bin/coffee +bin/escodegen +bin/esgenerate +bin/mocha + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a08daae --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2014, Elf M. Sternberg + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Elf M. Sternberg nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..10e189d --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: test + +# docs: $(patsubst %.md,%.html,$(wildcard *.md)) + +%.html: %.md header.html footer.html + cat header.html > $@ + pandoc $< >> $@ + cat footer.html >> $@ + +node_modules: package.json + mkdir -p node_modules + npm install + +test: node_modules + @node_modules/.bin/mocha --compilers coffee:coffee-script/register + diff --git a/README.md b/README.md new file mode 100644 index 0000000..314ab90 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# A simple implementation of Lisp-like cons() lists, using vectors + +## Purpose + +I kinda got tired of my broken list implementations that I was working +with in each and every variant of Lisp I wrote while working my way +through List In Small Pieces, so I decided to break it out into its own +managed repo. + +## LICENSE AND COPYRIGHT NOTICE: NO WARRANTY GRANTED OR IMPLIED + +Copyright (c) 2015 Elf M. Sternberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +- Elf M. Sternberg diff --git a/bin/activate b/bin/activate new file mode 100644 index 0000000..d074497 --- /dev/null +++ b/bin/activate @@ -0,0 +1,8 @@ +#!/bin/bash + +# /bin comes before /node_modules/.bin because sometimes I want to +# override the behaviors provided. + +PROJECT_ROOT=`pwd` +PATH="$PROJECT_ROOT/bin:$PROJECT_ROOT/node_modules/.bin:$PATH" +export PATH diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee05643 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "cons-lists", + "version": "0.0.1", + "description": "Cons-style lists for Javascript", + "main": "lists.js", + "directories": { + "test": "test" + }, + "dependencies": { + }, + "devDependencies": { + "chai": "^2.0.0", + "mocha": "^2.1.0" + }, + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "ssh://elfsternberg@elfsternberg.com/home/elfsternberg/repos/cons-lists" + }, + "keywords": [ + "lisp", + "javascript", + "coffeescript" + ], + "author": "Elf M. Sternberg ", + "license": "MIT" +} diff --git a/src/lists.coffee b/src/lists.coffee new file mode 100644 index 0000000..451f6fc --- /dev/null +++ b/src/lists.coffee @@ -0,0 +1,81 @@ +vectorp = (a) -> toString.call(a) == '[object Array]' + +cellp = (a) -> vectorp(a) and a.__list == true + +pairp = (a) -> cellp(a) and (a.length == 2) + +listp = (a) -> (pairp a) and (pairp cdr a) + +recordp = (a) -> Object.prototype.toString.call(a) == '[object Object]' + +nilp = (a) -> cellp(a) and a.length == 0 + +nil = (-> l = []; l.__list = true; l)() + +cons = (a, b = nil) -> + l = if not (a?) then [] else if (nilp a) then b else [a, b] + l.__list = true + l + +car = (a) -> a[0] + +cdr = (a) -> a[1] + +vectorToList = (v, p) -> + p = if p? then p else 0 + if p >= v.length then return nil + # Annoying, but since lists are represented as nested arrays, they + # have to be intercepted first. The use of duck-typing here is + # frustrating, but I suppose the eventual runtime will be doing + # something like this anyway for base types. + item = if pairp(v[p]) then v[p] else if vectorp(v[p]) then vectorToList(v[p]) else v[p] + cons(item, vectorToList(v, p + 1)) + +list = (v...) -> + ln = v.length; + (nl = (a) -> + cons(v[a], if (a < ln) then (nl(a + 1)) else nil))(0) + +listToVector = (l, v = []) -> + return v if nilp l + v.push if pairp (car l) then listToVector(car l) else (car l) + listToVector (cdr l), v + +# This is the simplified version. It can't be used stock with reader, +# because read() returns a rich version decorated with extra +# information. + +metacadr = (m) -> + seq = m.match(/c([ad]+)r/)[1].split('') + return (l) -> + inner = (l, s) -> + return nil if nilp l + return l if s.length == 0 + inner ((if s.pop() == 'a' then car else cdr)(l)), s + inner(l, seq) + +module.exports = + cons: cons + nil: nil + car: car + cdr: cdr + list: list + nilp: nilp + pairp: pairp + vectorp: vectorp + recordp: recordp + vectorToList: vectorToList + listToVector: listToVector + setcar: (a, l) -> l[0] = a; a + setcdr: (a, l) -> l[1] = a; a + cadr: (l) -> car (cdr l) + cddr: (l) -> cdr (cdr l) + cdar: (l) -> cdr (car l) + caar: (l) -> car (car l) + caddr: (l) -> car (cdr (cdr l)) + cdddr: (l) -> cdr (cdr (cdr l)) + cadar: (l) -> car (cdr (car l)) + cddar: (l) -> cdr (cdr (car l)) + caadr: (l) -> car (car (cdr l)) + cdadr: (l) -> cdr (car (cdr l)) + metacadr: metacadr diff --git a/src/reduce.coffee b/src/reduce.coffee new file mode 100644 index 0000000..4c53cf6 --- /dev/null +++ b/src/reduce.coffee @@ -0,0 +1,60 @@ +{car, cdr, cons, listp, pairp, nilp, nil, list, listToString} = require './lists' + +reduce = (lst, iteratee, memo, context) -> + count = 0 + return memo if nilp lst + memo = iteratee.call(context, memo, (car lst), count) + lst = cdr lst + count++ + while not nilp lst + memo = iteratee.call(context, memo, (car lst), count) + count++ + lst = cdr lst + null + memo + +map = (lst, iteratee, context) -> + return nil if nilp lst + root = cons() + + reducer = (memo, item, count) -> + next = cons(iteratee.call(context, item, count, lst)) + memo[1] = next + next + + reduce(lst, reducer, root, context) + (cdr root) + +rmap = (lst, iteratee, context) -> + return nil if nilp lst + root = cons() + + reducer = (memo, item, count) -> + cons(iteratee.call(context, item, count, lst), memo) + + reduce(lst, reducer, root, context) + +filter = (lst, iteratee, context) -> + return nil if nilp lst + root = cons() + + reducer = (memo, item, count) -> + if iteratee.call(context, item, count, lst) + next = cons(item) + memo[1] = next + next + else + memo + + reduce(lst, reducer, root, context) + if (pairp root) then (cdr root) else root + +reverse = (lst) -> reduce(lst, ((memo, value) -> cons(value, memo)), cons()) + +module.exports = + reduce: reduce + map: map + rmap: rmap + filter: filter + reverse: reverse + diff --git a/test/lists.coffee b/test/lists.coffee new file mode 100644 index 0000000..a102c72 --- /dev/null +++ b/test/lists.coffee @@ -0,0 +1,75 @@ +chai = require 'chai' +chai.should() +expect = chai.expect + +{listToVector, vectorToList, cons, list, nil, metacadr} = require '../src/lists' + +describe "Basic list building", -> + for [t, v] in [ + [cons(), cons()] + [cons(nil), cons()] + [cons(), cons(nil)] + [cons('a'), cons('a')] + [cons('a', cons('b', cons('c'))), cons('a', cons('b', cons('c')))] + [cons('a', cons('b', cons('c'))), cons('a', cons('b', cons('c', nil)))] + [cons('a', cons('b', cons('c', nil))), cons('a', cons('b', cons('c')))] + [cons(nil, cons('a')), cons('a')] # Test for identity; consing nil to anything results in anything + [cons(nil, cons(nil, cons(nil))), nil]] + do (t, v) -> + it "should match #{t}", -> + expect(t).to.deep.equal(v) + +describe 'Round trip equivalence', -> + for [t, v] in [ + [[], []] + [['a'], ['a']] + [['a', 'b'], ['a', 'b']] + [['a', 'b', 'c'], ['a', 'b', 'c']]] + do (t, v) -> + it "should successfully round-trip #{t}", -> + expect(listToVector vectorToList t).to.deep.equal(v) + +describe 'List Building', -> + for [t, v] in [ + [cons(), []] + [cons(nil), []] + [cons('a'), ['a']] + [cons('a', cons('b')), ['a', 'b']] + [cons('a', cons('b', cons('c'))), ['a', 'b', 'c']] + [cons('a', cons('b', cons('c'), nil)), ['a', 'b', 'c']]] + do (t, v) -> + it "should cons a list into #{v}", -> + expect(listToVector t).to.deep.equal(v) + +describe 'Dynamic list constructor', -> + for [t, v] in [ + [list(), []] + [list('a'), ['a']] + [list('a', 'b'), ['a', 'b']] + [list('a', 'b', 'c'), ['a', 'b', 'c']]] + do (t, v) -> + it "should round trip list arguments into #{v}", -> + expect(listToVector t).to.deep.equal(v) + +mcsimple = cons('a', cons('b', cons('c'))) + +describe 'Metacadr Simple', -> + for [t, v, r] in [ + ['car', 'a'] + ['cadr', 'b'] + ['caddr', 'c']] + do (t, v) -> + it "The #{t} should read #{v}", -> + expect(metacadr(t)(mcsimple)).to.equal(v) + +mccomplex = vectorToList([['a', 'b', 'c'], ['1', '2', '3'], ['X', 'Y', 'Z'], ['f', 'g', 'h']]) + +describe 'Metacadr Complex', -> + for [t, v, r] in [ + ['cadar', 'b'] + ['caadddr', 'f'] + ['caadr', '1'] + ['caaddr', 'X']] + do (t, v) -> + it "The #{t} should read #{v}", -> + expect(metacadr(t)(mccomplex)).to.equal(v) diff --git a/test/reduce.coffee b/test/reduce.coffee new file mode 100644 index 0000000..1d1d323 --- /dev/null +++ b/test/reduce.coffee @@ -0,0 +1,56 @@ +chai = require 'chai' +chai.should() +expect = chai.expect + +{listToVector, vectorToList, listToString, cons, list, nil} = require '../src/lists' +{map, reduce, filter, reverse} = require '../src/reduce' + +id = (item) -> item + +describe 'Map Identity Testing', -> + + samples = [ + cons(), + cons(nil), + cons('a'), + cons('a', cons('b')) + cons('a', cons('b', cons('c'))), + cons('a', cons('b', cons('c'), nil))] + + for t in samples + do (t) -> + it "should produce the same thing as #{t}", -> + product = map(t, id) + expect(product).to.deep.equal(t) + +describe 'Filter Testing Testing', -> + + samples = [ + [vectorToList([]), nil], + [vectorToList([1]), nil], + [vectorToList([1, 2]), cons(2)], + [vectorToList([1, 2, 3]), cons(2)], + [vectorToList([1, 2, 3 ,4]), cons(2, cons(4))]] + + truth = (item) -> item % 2 == 0 + + for [t, v] in samples + do (t, v) -> + it "should produce the same thing as #{v}", -> + product = filter(t, truth) + expect(product).to.deep.equal(v) + +describe 'Reverse', -> + + samples = [ + [cons(), cons()] + [cons(nil), cons(nil)] + [cons('a'), cons('a')] + [cons('a', cons('b')), cons('b', cons('a'))] + [cons('a', cons('b', cons('c'))), cons('c', cons('b', cons('a')))]] + + for [t, v] in samples + do (t, v) -> + it "#{t} should produce a reverse of #{v}", -> + product = reverse(t) + expect(product).to.deep.equal(v)