diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..243673f --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in ruby_ukanren.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..05ca57f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Justin Leitgeb + +MIT License + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e8a413 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# RubyUkanren + +TODO: Write a gem description + +## Installation + +Add this line to your application's Gemfile: + + gem 'ruby_ukanren' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install ruby_ukanren + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it ( http://github.com//ruby_ukanren/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c6c3f91 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +require "bundler/gem_tasks" + +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs << "spec" + t.test_files = FileList['spec/**/*_spec.rb'] +end + +task :default => :test diff --git a/lib/ukanren.rb b/lib/ukanren.rb new file mode 100644 index 0000000..0f29343 --- /dev/null +++ b/lib/ukanren.rb @@ -0,0 +1,3 @@ +require "ukanren/version" +require "ukanren/lisp" +require "ukanren/language" diff --git a/lib/ukanren/language.rb b/lib/ukanren/language.rb new file mode 100644 index 0000000..0365e3c --- /dev/null +++ b/lib/ukanren/language.rb @@ -0,0 +1,104 @@ +module Ukanren + module Language + include Lisp + + def var(*c) ; Array.new(c) ; end + def var?(x) ; x.is_a?(Array) ; end + + def vars_eq?(x1, x2) ; x1 == x2 ; end + + def mzero ; nil ; end + + def ext_s(x, v, s) + cons(cons(x, v), s) + end + + def unify(u, v, s) + u = walk(u, s) + v = walk(v, s) + + if var?(u) && var?(v) && vars_eq?(u, v) + s + elsif var?(u) + ext_s(u, v, s) + + elsif var?(v) + ext_s(v, u, s) + + elsif pair?(u) && pair?(v) + if s = unify(car(u), car(v), s) + unify(cdr(u), cdr(v), s) + end + elsif u == v + s + end + end + + # -- Constrain u to be equal to v. + def eq(u, v) + ->(s_c) { + s = unify(u, v, car(s_c)) + s ? unit(cons(s, cdr(s_c))) : mzero + } + end + + # Walk environment S and look up value of U, if present. + def walk(u, s) + if var?(u) then + pr = assp(-> (v) { u == v }, s) + pr ? walk(cdr(pr), s) : u + + else + u + end + end + + # Call function f with a fresh variable. + def call_fresh(f) + -> (s_c) { + c = cdr(s_c) + f.call(var(c)).call(cons(car(s_c), c + 1)) + } + end + + def mplus(d1, d2) + if d1.nil? + d2 + elsif d1 == mzero + d2 + elsif d1.is_a?(Proc) + -> { mplus(d2, d1.call) } + else + cons(car(d1), mplus(cdr(d1), d2)) + end + end + + def bind(d, g) + if d.nil? + mzero + elsif d == mzero + mzero + elsif d.is_a?(Proc) + -> { bind(d.call, g) } + else + mplus(g(car(d)), bind(cdr(d), g)) + end + end + + def disj(g1, g2) + -> (s_c) { + mplus(g1(s_c), g2(s_c)) + } + end + + def conj(g1, g2) + -> (s_c) { + bind(g1(s_c), g2) + } + end + + def empty_env + cons(mzero, 0) + end + end +end diff --git a/lib/ukanren/lisp.rb b/lib/ukanren/lisp.rb new file mode 100644 index 0000000..9de9576 --- /dev/null +++ b/lib/ukanren/lisp.rb @@ -0,0 +1,21 @@ +module Ukanren + module Lisp + + def cons(x, y) ; -> (m) { m.call(x, y) } ; end + def car(z) ; z.call(-> (p, q) { p }) ; end + def cdr(z) ; z.call(-> (p, q) { q }) ; end + + # Search association list by predicate function. + # Based on lua implementation by silentbicycle: + # https://github.com/silentbicycle/lua-ukanren/blob/master/ukanren.lua#L53:L61 + # + # Additional reference for this function is scheme: + # Ref for assp: http://www.r6rs.org/final/html/r6rs-lib/r6rs-lib-Z-H-4.html + def assp(func, alist) + if alist && hd = car(alist) + func.call(hd) ? hd : assp(func, cdr(alist)) + end + end + + end +end diff --git a/lib/ukanren/version.rb b/lib/ukanren/version.rb new file mode 100644 index 0000000..93d6e4b --- /dev/null +++ b/lib/ukanren/version.rb @@ -0,0 +1,3 @@ +module Ukanren + VERSION = "0.0.1" +end diff --git a/ruby_ukanren.gemspec b/ruby_ukanren.gemspec new file mode 100644 index 0000000..3e39ffe --- /dev/null +++ b/ruby_ukanren.gemspec @@ -0,0 +1,24 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'ukanren/version' + +Gem::Specification.new do |spec| + spec.name = "ruby_ukanren" + spec.version = Ukanren::VERSION + spec.authors = ["Justin Leitgeb"] + spec.email = ["justin@stackbuilders.com"] + spec.summary = %q{uKanren in Ruby} + spec.description = %q{Implements uKanren paper in Ruby} + spec.homepage = "" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.5" + spec.add_development_dependency "rake" + spec.add_development_dependency 'mocha' +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..b1f202a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,5 @@ +require 'minitest/autorun' + +require "mocha" + +require "#{File.dirname(__FILE__)}/../lib/ukanren" diff --git a/spec/ukanren/language_spec.rb b/spec/ukanren/language_spec.rb new file mode 100644 index 0000000..8cd8dc4 --- /dev/null +++ b/spec/ukanren/language_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Ukanren::Language do + include Ukanren::Language + + describe "second-set t1" do + it "returns one binding" do + call_fresh(-> (q) { eq(q, 5) }).call(empty_env) + end + end +end diff --git a/spec/ukanren/lisp_spec.rb b/spec/ukanren/lisp_spec.rb new file mode 100644 index 0000000..393c950 --- /dev/null +++ b/spec/ukanren/lisp_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Ukanren::Lisp do + include Ukanren::Lisp + + describe "#car" do + it "returns the first element in the pair" do + car(cons(1, 2)).must_equal 1 + end + end + + describe "#cdr" do + it "returns the second item in the pair" do + cdr(cons(1, 2)).must_equal 2 + end + end + + describe "#cons" do + it "returns a pair" do + car(cons(1, 2)).must_equal 1 + cdr(cons(1, 2)).must_equal 2 + end + end + + describe "#assp" do + it "returns the first element for which the predicate function is true" do + alist = cons(1, cons(2, cons(3, 4))) + assp(->(i) { i == 3 }, alist).must_equal 3 + end + end +end