commit 2bee084be3a5e9caf22db3d9d43ea0e18f4d2744 Author: Mike Cifelli Date: Sat Jan 26 14:09:00 2019 -0500 Initial commit diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..08db616 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "code-runner.customCommand": "ruby test/mutual_recursion_test.rb", + "code-runner.runInTerminal": true +} \ No newline at end of file diff --git a/lib/mutual_recursion.rb b/lib/mutual_recursion.rb new file mode 100644 index 0000000..f83f746 --- /dev/null +++ b/lib/mutual_recursion.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module MutualRecursion + UNEXPECTED_TYPE = 'expected tail_call or terminal_value' + + class TailCall + attr_reader :value, :block + + def initialize(value = nil, &block) + @value = value + @block = block + end + + def invoke + self.then do |tail| + while tail.block + tail = tail.block.call + raise TypeError, UNEXPECTED_TYPE unless tail.is_a?(TailCall) + end + + tail.value + end + end + end +end + +def tail_call + MutualRecursion::TailCall.new { yield } +end + +def terminal_value(value) + MutualRecursion::TailCall.new(value) +end diff --git a/mutual_recursion.gemspec b/mutual_recursion.gemspec new file mode 100644 index 0000000..0ec7766 --- /dev/null +++ b/mutual_recursion.gemspec @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = 'mutual_recursion' + s.version = '0.0.1' + s.date = '2019-01-26' + s.summary = 'Mutual Recursion for Ruby' + s.description = 'Tail call optimization for mutually (and directly) recursive functions using a trampoline.' + s.authors = ['Mike'] + s.files = ['lib/mutual_recursion.rb'] + s.homepage = '' + s.license = 'MIT' +end diff --git a/test/mutual_recursion_test.rb b/test/mutual_recursion_test.rb new file mode 100644 index 0000000..0b7e5af --- /dev/null +++ b/test/mutual_recursion_test.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'minitest/pride' + +require_relative '../lib/mutual_recursion' + +class InventoryTest < Minitest::Test + def test_terminal_value + tail = terminal_value(42) + assert_equal(42, tail.invoke) + end + + def test_single_recursion + tail = tail_call { terminal_value(42) } + assert_equal(42, tail.invoke) + end + + def test_several_recursions + tail = tail_call { tail_call { tail_call { terminal_value(42) } } } + assert_equal(42, tail.invoke) + end + + def test_mutual_tail_recursion + tail = one(50_000) + assert_equal(50_001, tail.invoke) + end + + def test_direct_tail_recursion + tail = direct(50_000) + assert_equal(50_001, tail.invoke) + end + + def test_recursive_function_can_return_lambda + tail = lambda_returning + assert_kind_of(Proc, tail.invoke) + end + + def test_recursive_function_can_return_proc + tail = proc_returning + assert_kind_of(Proc, tail.invoke) + end + + def test_non_tail_call_detected + tail = bad_return + assert_raises(TypeError) { tail.invoke } + end +end + +class TailCallDecoy + attr_reader :value, :block + + def initialize + @value = 99 + @block = proc { 25 } + end +end + +def one(x, y = 0) + return terminal_value(y) if x.negative? + + tail_call { two(x, y + 1) } +end + +def two(x, y) + tail_call { one(x - 1, y) } +end + +def direct(x, y = 0) + return terminal_value(y) if x.negative? + + tail_call { direct(x - 1, y + 1) } +end + +def lambda_returning + terminal_value(-> { 24 }) +end + +def proc_returning + terminal_value(proc { 24 }) +end + +def bad_return + tail_call { TailCallDecoy.new } +end