Move functions into module

This commit is contained in:
Mike Cifelli 2019-01-27 11:55:52 -05:00
parent f4492cf8cd
commit 01b1d4c7e3
6 changed files with 74 additions and 23 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
*.swp *.swp
*.gem *.gem
.yardoc/
doc/

View File

@ -1,4 +1,5 @@
{ {
"code-runner.customCommand": "rake test", "code-runner.customCommand": "rake test",
"code-runner.ignoreSelection": true,
"code-runner.runInTerminal": true "code-runner.runInTerminal": true
} }

View File

@ -2,10 +2,12 @@
Tail call optimization for mutually (and directly) recursive functions in Ruby. 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 ### examples
```ruby ```ruby
require 'mutual_recursion' require 'mutual_recursion'
include MutualRecursion
def mutual_one(x, y = 0) def mutual_one(x, y = 0)
return terminal_value(y) if x.negative? return terminal_value(y) if x.negative?
@ -22,6 +24,7 @@ mutual_one(50_000).invoke
``` ```
```ruby ```ruby
require 'mutual_recursion' require 'mutual_recursion'
include MutualRecursion
def direct(x, y = 0) def direct(x, y = 0)
return terminal_value(y) if x.negative? return terminal_value(y) if x.negative?
@ -34,6 +37,7 @@ direct(50_000).invoke
``` ```
```ruby ```ruby
require 'mutual_recursion' require 'mutual_recursion'
include MutualRecursion
def proc_returning(x, y = 0) def proc_returning(x, y = 0)
return terminal_value(proc { "|#{y}|" }) if x.negative? 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) } tail_call { proc_returning(x - 1, y + 1) }
end end
proc_returning(20).invoke.call generated_proc = proc_returning(20).invoke
generated_proc.call
# => "|21|" # => "|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
```

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module MutualRecursion module MutualRecursion
UNEXPECTED_TYPE = 'expected tail_call or terminal_value'
class TailCall class TailCall
attr_reader :value, :block attr_reader :value, :block
@ -11,23 +9,49 @@ module MutualRecursion
@block = block @block = block
end 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 def invoke
self.then do |tail| self.then do |tail|
while tail.block while tail.block
tail = tail.block.call tail = tail.block.call
raise TypeError, UNEXPECTED_TYPE unless tail.is_a?(TailCall) raise MissingTailCallError unless tail.is_a?(TailCall)
end end
tail.value tail.value
end end
end end
end end
end
def tail_call class MissingTailCallError < StandardError
MutualRecursion::TailCall.new { yield } def initialize(msg = 'expected a tail call')
end super
end
end
def terminal_value(value) module_function
MutualRecursion::TailCall.new(value)
##
# 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 end

View File

@ -5,7 +5,7 @@ Gem::Specification.new do |s|
s.version = '0.0.1' s.version = '0.0.1'
s.date = '2019-01-26' s.date = '2019-01-26'
s.summary = 'Mutual Recursion for Ruby' 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.authors = ['Mike']
s.files = ['lib/mutual_recursion.rb'] s.files = ['lib/mutual_recursion.rb']
s.homepage = 'https://gitlab.com/mike-cifelli/mutual_recursion' s.homepage = 'https://gitlab.com/mike-cifelli/mutual_recursion'

View File

@ -6,6 +6,8 @@ require 'minitest/pride'
require_relative '../lib/mutual_recursion' require_relative '../lib/mutual_recursion'
class InventoryTest < Minitest::Test class InventoryTest < Minitest::Test
include MutualRecursion
def test_terminal_value def test_terminal_value
tail = terminal_value(42) tail = terminal_value(42)
assert_equal(42, tail.invoke) assert_equal(42, tail.invoke)
@ -43,16 +45,12 @@ class InventoryTest < Minitest::Test
def test_non_tail_call_detected def test_non_tail_call_detected
tail = bad_return tail = bad_return
assert_raises(TypeError) { tail.invoke } assert_raises(MissingTailCallError) { tail.invoke }
end end
end
class TailCallDecoy def test_module_functions
attr_reader :value, :block tail = MutualRecursion.tail_call { MutualRecursion.terminal_value(42) }
assert_equal(42, tail.invoke)
def initialize
@value = 99
@block = proc { 25 }
end end
end end
@ -77,11 +75,20 @@ def lambda_returning
end end
def proc_returning(x, y = 0) 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) } tail_call { proc_returning(x - 1, y + 1) }
end end
def bad_return 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 end