diff --git a/.gitignore b/.gitignore index 2fb4efe..e2c3d48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.swp -*.gem \ No newline at end of file +*.gem +.yardoc/ +doc/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index ad649f7..cd37d1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "code-runner.customCommand": "rake test", + "code-runner.ignoreSelection": true, "code-runner.runInTerminal": true } \ No newline at end of file diff --git a/README.md b/README.md index 22b1450..2672767 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ Tail call optimization for mutually (and directly) recursive functions in Ruby. -The current desing uses a trampoline. However, it is implemented in a way that still allows a tail recursive function to return a Proc as its terminal value without it being called prematurely by accident. +The current design uses a trampoline. However, it is implemented in a way that still allows a tail recursive function to return a Proc as its terminal value. + ### examples ```ruby require 'mutual_recursion' +include MutualRecursion def mutual_one(x, y = 0) return terminal_value(y) if x.negative? @@ -22,6 +24,7 @@ mutual_one(50_000).invoke ``` ```ruby require 'mutual_recursion' +include MutualRecursion def direct(x, y = 0) return terminal_value(y) if x.negative? @@ -34,6 +37,7 @@ direct(50_000).invoke ``` ```ruby require 'mutual_recursion' +include MutualRecursion def proc_returning(x, y = 0) return terminal_value(proc { "|#{y}|" }) if x.negative? @@ -41,6 +45,19 @@ def proc_returning(x, y = 0) tail_call { proc_returning(x - 1, y + 1) } end -proc_returning(20).invoke.call +generated_proc = proc_returning(20).invoke +generated_proc.call # => "|21|" ``` +```ruby +require 'mutual_recursion' + +def without_include(x, y = 0) + return MutualRecursion.terminal_value(y) if x.negative? + + MutualRecursion.tail_call { without_include(x - 1, y + 1) } +end + +without_include(50_000).invoke +# => 50001 +``` diff --git a/lib/mutual_recursion.rb b/lib/mutual_recursion.rb index f83f746..9cfffbe 100644 --- a/lib/mutual_recursion.rb +++ b/lib/mutual_recursion.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module MutualRecursion - UNEXPECTED_TYPE = 'expected tail_call or terminal_value' - class TailCall attr_reader :value, :block @@ -11,23 +9,49 @@ module MutualRecursion @block = block end + ## + # Invoke this tail call. Only the returned value from the initial call to + # the recursive function should be invoked. + # + # @return [Object] the terminal_value of this tail call + def invoke self.then do |tail| while tail.block tail = tail.block.call - raise TypeError, UNEXPECTED_TYPE unless tail.is_a?(TailCall) + raise MissingTailCallError unless tail.is_a?(TailCall) end tail.value end end end -end -def tail_call - MutualRecursion::TailCall.new { yield } -end + class MissingTailCallError < StandardError + def initialize(msg = 'expected a tail call') + super + end + end -def terminal_value(value) - MutualRecursion::TailCall.new(value) + module_function + + ## + # Make a direct or indirect recursive call in tail position. + # + # @yieldreturn [MutualRecursion::TailCall] a tail call + # @return [MutualRecursion::TailCall] a non terminal tail call + + def tail_call + TailCall.new { yield } + end + + ## + # Indicates that the recursion has ended with the provided value. + # + # @param [Object] value the terminal value + # @return [MutualRecursion::TailCall] a terminal tail call that will return the given value + + def terminal_value(value) + TailCall.new(value) + end end diff --git a/mutual_recursion.gemspec b/mutual_recursion.gemspec index a4de4d5..dfffbf4 100644 --- a/mutual_recursion.gemspec +++ b/mutual_recursion.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |s| 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.description = 'Tail call optimization for mutually (and directly) recursive functions.' s.authors = ['Mike'] s.files = ['lib/mutual_recursion.rb'] s.homepage = 'https://gitlab.com/mike-cifelli/mutual_recursion' diff --git a/test/mutual_recursion_test.rb b/test/mutual_recursion_test.rb index 4f5fa80..eda2344 100644 --- a/test/mutual_recursion_test.rb +++ b/test/mutual_recursion_test.rb @@ -6,6 +6,8 @@ require 'minitest/pride' require_relative '../lib/mutual_recursion' class InventoryTest < Minitest::Test + include MutualRecursion + def test_terminal_value tail = terminal_value(42) assert_equal(42, tail.invoke) @@ -43,16 +45,12 @@ class InventoryTest < Minitest::Test def test_non_tail_call_detected tail = bad_return - assert_raises(TypeError) { tail.invoke } + assert_raises(MissingTailCallError) { tail.invoke } end -end -class TailCallDecoy - attr_reader :value, :block - - def initialize - @value = 99 - @block = proc { 25 } + def test_module_functions + tail = MutualRecursion.tail_call { MutualRecursion.terminal_value(42) } + assert_equal(42, tail.invoke) end end @@ -77,11 +75,20 @@ def lambda_returning end def proc_returning(x, y = 0) - return terminal_value(proc { puts "|#{y}|" }) if x.negative? + return terminal_value(proc { "|#{y}|" }) if x.negative? tail_call { proc_returning(x - 1, y + 1) } end def bad_return - tail_call { TailCallDecoy.new } + tail_call do + Class.new do + attr_reader :value, :block + + def initialize + @value = 99 + @block = proc { 25 } + end + end.new + end end