diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..5be63fc --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--format documentation diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..747c9a6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +group :development, :test do + gem "rspec", "~> 3.13" + gem "rubocop", require: false + gem "rake" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..0ed9a5b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,60 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.3) + diff-lcs (1.6.2) + json (2.13.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + prism (1.4.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.3.0) + regexp_parser (2.11.2) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.5) + rubocop (1.80.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + unicode-display_width (3.1.5) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + rake + rspec (~> 3.13) + rubocop + +BUNDLED WITH + 2.6.2 diff --git a/README.md b/README.md index ebf5020..2e998e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ -#Suchimer -suchi's sucheme by ruby (2010/12/21-) -Sorry, test. Please ignore this. +## Suchemer +sucheme in Ruby (original: 2010/12/21-). This repo now includes a simple Lisp-style Cons implementation. + +### Cons usage +```ruby +require "suchemer/cons" +include Suchemer + +lst = Cons.list(1,2,3) # => (1 2 3) +lst.to_a # => [1,2,3] +lst[1] # => 2 +lst.reverse # => (3 2 1) +lst.append([4,5]) # => (1 2 3 4 5) + +Cons.new(1, 2) # => (1 . 2) +``` + +See specs under `spec/` for more. diff --git a/lib/suchemer/cons.rb b/lib/suchemer/cons.rb index efe6b64..a9ade18 100644 --- a/lib/suchemer/cons.rb +++ b/lib/suchemer/cons.rb @@ -1,8 +1,192 @@ module Suchemer class Cons + # A simple Lisp-style cons cell with basic list operations. + # - A proper list is a chain of Cons ending in nil. + # - A dotted pair is a Cons whose cdr is neither Cons nor nil. + + include Enumerable + attr_accessor :car, :cdr - def initialize(car, cdr) - @car, @cdr = car, cdr + + def initialize(car = nil, cdr = nil) + @car = car + @cdr = cdr end + + # Build a proper list from given elements. + def self.list(*elements) + from_array(elements) + end + + # Build a proper list from an Array or any Enumerable. + def self.from_array(enum) + arr = enum.respond_to?(:to_a) ? enum.to_a : Array(enum) + node = nil + arr.reverse_each { |e| node = new(e, node) } + node + end + + class << self + alias from_enum from_array + alias pair new + end + + # Iterate over car values of a proper list. Stops before a dotted tail if any. + def each + return enum_for(:each) unless block_given? + node = self + while node.is_a?(Cons) + yield node.car + node = node.cdr + end + self + end + + # Convert to a Ruby Array (drops a dotted tail if present). + def to_a + map { |x| x } + end + + # Number of elements in the proper part of the list. + def size + count + end + alias length size + + # Whether this is a proper list (cdr chain ends with nil). + def proper_list? + node = self + while node.is_a?(Cons) + node = node.cdr + end + node.nil? + end + + # Whether this cons is a dotted pair (cdr is not Cons and not nil). + def dotted_pair? + !(@cdr.is_a?(Cons) || @cdr.nil?) + end + + # Array-like element access. Supports negative indices via conversion. + def [](index) + unless index.is_a?(Integer) + raise TypeError, "index must be Integer" + end + return to_a[index] if index.negative? + + i = 0 + node = self + while node.is_a?(Cons) && i < index + node = node.cdr + i += 1 + end + return nil unless node.is_a?(Cons) + node.car + end + alias at [] + + # Return the last element of a proper list. For a dotted pair chain, + # returns the cdr of the final cons. + def last + node = self + prev = nil + while node.is_a?(Cons) + prev = node + node = node.cdr + end + # node is now either nil (proper list) or dotted tail + if node.nil? + prev&.car + else + # dotted tail value + node + end + end + + # Return a new proper list with same elements in reverse order. + # Raises ArgumentError if not a proper list. + def reverse + raise ArgumentError, "reverse requires a proper list" unless proper_list? + acc = nil + node = self + while node.is_a?(Cons) + acc = Cons.new(node.car, acc) + node = node.cdr + end + acc + end + + # Append another list/array to this list, returning a new list. + # Accepts: Cons, Array, or nil. Requires this to be a proper list. + def append(other) + raise ArgumentError, "append requires a proper list on the left-hand side" unless proper_list? + + rhs = + case other + when nil + nil + when Cons + other + when Array + Cons.from_array(other) + else + raise ArgumentError, "append accepts Cons, Array, or nil" + end + + # Copy this list and attach rhs at the end. + return rhs if self.nil? + # copy head + head = Cons.new(@car, nil) + dst = head + src = @cdr + while src.is_a?(Cons) + dst.cdr = Cons.new(src.car, nil) + dst = dst.cdr + src = src.cdr + end + dst.cdr = rhs + head + end + + # Structural equality. Compares element-wise for proper lists. + # Also supports comparison with Arrays. + def ==(other) + case other + when Cons + a = self + b = other + while a.is_a?(Cons) && b.is_a?(Cons) + return false unless a.car == b.car + a = a.cdr + b = b.cdr + end + a.nil? && b.nil? + when Array + to_a == other + else + false + end + end + + # Pretty print like Lisp: (1 2 3) or (1 . 2) + def to_s + parts = [] + node = self + while node.is_a?(Cons) + parts << node.car.inspect + node = node.cdr + end + if node.nil? + "(" + parts.join(" ") + ")" + else + # dotted tail + "(" + parts.join(" ") + " . #{node.inspect})" + end + end + alias inspect to_s + + # Convenience aliases for lispy naming + alias head car + alias tail cdr end end diff --git a/spec/cons_spec.rb b/spec/cons_spec.rb new file mode 100644 index 0000000..e6830ab --- /dev/null +++ b/spec/cons_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Suchemer + RSpec.describe Cons do + describe ".list/.from_array" do + it "builds a proper list" do + lst = Cons.list(1, 2, 3) + expect(lst.to_a).to eq [1, 2, 3] + expect(lst.proper_list?).to be true + end + end + + describe "Enumerable" do + it "iterates over car values" do + expect(Cons.list(1,2,3).map { |x| x * 2 }).to eq [2,4,6] + end + end + + describe "indexing" do + it "supports positive and negative indices" do + lst = Cons.list(1,2,3) + expect(lst[0]).to eq 1 + expect(lst[1]).to eq 2 + expect(lst[-1]).to eq 3 + end + end + + describe "reverse" do + it "reverses a proper list" do + expect(Cons.list(1,2,3).reverse.to_a).to eq [3,2,1] + end + + it "raises on dotted list" do + dotted = Cons.new(1, 2) + expect { dotted.reverse }.to raise_error(ArgumentError) + end + end + + describe "append" do + it "appends array or cons" do + expect(Cons.list(1,2).append([3,4]).to_a).to eq [1,2,3,4] + expect(Cons.list(1,2).append(Cons.list(3,4)).to_a).to eq [1,2,3,4] + end + end + + describe "equality" do + it "compares structurally" do + expect(Cons.list(1,2,3)).to eq Cons.new(1, Cons.new(2, Cons.new(3, nil))) + expect(Cons.list(1,2,3)).to eq [1,2,3] + end + end + + describe "printing" do + it "prints proper and dotted lists" do + expect(Cons.list(1,2,3).to_s).to eq "(1 2 3)" + expect(Cons.new(1, 2).to_s).to eq "(1 . 2)" + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..a598e84 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "rspec" +require_relative "../lib/suchemer/cons" + +RSpec.configure do |config| + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.mock_with :rspec do |m| + m.verify_partial_doubles = true + end +end