Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--require spec_helper
--format documentation
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

188 changes: 186 additions & 2 deletions lib/suchemer/cons.rb
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions spec/cons_spec.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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