Skip to content

Instantly share code, notes, and snippets.

@havenwood
Last active March 5, 2025 04:59
Show Gist options
  • Save havenwood/55439b0b4666e4f1ea59eb0a3837209b to your computer and use it in GitHub Desktop.
Save havenwood/55439b0b4666e4f1ea59eb0a3837209b to your computer and use it in GitHub Desktop.
An example Ruby unary method implementation of a Schönhage–Strassen optimized factorial
##
# This refinement uses a binary-splitting algorithm for fairly
# efficient factorial calculation with results cached by default.
#
# Caching dramatically improves performance when portions of the
# factorial calculation are repeated.
class UnaryFactorial < Module
class NegativeError < StandardError
def initialize(message = 'Factorials cannot be negative') = super
end
attr_accessor :cache
##
# If the `cache` keyword argument doesn't provide a custom cache,
# a `Hash` will be used as the default cache. Set `cache` to `nil`
# to disable caching.
#
# A custom `cache` must respond to `[]` and `[]=` and should also
# respond to `size`, `clear` and `delete` as an accessor interface.
def initialize(cache: {})
@cache = cache
module_eval do
refine Integer do
def !@
raise NegativeError if negative?
factorial(1..self)
end
private
define_method :factorial do |range|
if cache
value = cache[range]
return value if value
end
from = range.begin
to = range.end
diff = to - from
value = case diff
when 0
from
when ..3
range.reduce(1, :*)
else
mid = (to + from) / 2
factorial(from..mid) * factorial(mid.succ..to)
end
cache[range] = value if cache
value
end
end
end
end
end
require 'benchmark/ips'
Factorial = UnaryFactorial.new
using Factorial
Benchmark.ips do
it.report('Normal uncached 120') { (2..120).reduce(1, :*) }
it.report('Unary cached 120') { !120 }
end
# Factorial.cache #=> {1..4 => 24, 5..8 => 1680, 1..8 => 40320, ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment