Enhances modules to be functional by disabling the ability to extend, include, or prepend while also ensuring all methods are class methods. This allows you to use a module as a functional collection of related methods for reuse throughout your application via composition instead of inheritance. In other words, this is an enhanced version of module_function by preventing inheritance and reducing the memory overhead due to making copies of the original methods.
-
Allows modules to be a collection of functional methods.
-
Allows your modules and/or methods to be composable instead of inherited.
-
Provides faster performance and a reduced memory than any object within your application. See the Benchmarks section for details.
-
Ruby.
-
A solid understanding Ruby Modules.
To install with security, run:
# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install functionable --trust-policy HighSecurityTo install without security, run:
gem install functionableYou can also add the gem directly to your project:
bundle add functionableOnce the gem is installed, you only need to require it:
require "functionable"Enhancing your module to be functional is as simple as extending your module:
module Math
extend Functionable
def add(first, second) = first + second
def subtract(first, second) = first - second
def multiply(first, second) = first * second
def divide(first, second) = first / second
end
Math.add 6, 3 # 9
Math.subtract 6, 3 # 3
Math.multiply 6, 3 # 18
Math.divide 6, 3 # 2That’s it! Now you can add related methods, as desired, which are specific to your namespace.
Should you need to make any of your methods private, use the conceal method as follows:
module Demo
extend Functionable
def welcome(*) = print(*)
def print(message = "The default message.") = puts message
conceal :print
end
Demo.welcome # "The default message."
Demo.print # private method 'print' called for module Demo (NoMethodError)The conceal method takes the same parameters as the private_class_method which means you can use a string, symbol, a single argument, multiple arguments, or an array.
Functional modules are only meant to be a collection of related methods which allows you to namespace your behavior, compose multiple methods together, and use composition to inject your module as a dependency within objects that need this functionality.
This means the following behavior is disabled (each uses an anonymous module and/or class for demonstration purposes and reduced syntax).
Use composition instead of inheritance:
functions = Module.new.extend Functionable
Class.new.extend functions
# Module extend is disabled. (NoMethodError)Use composition instead of inheritance:
functions = Module.new.extend Functionable
Class.new.include functions
# Module include is disabled. (NoMethodError)You also can’t include Funtionable, only extend:
Module.new.include Functionable
# Module include is disabled, use extend instead. (NoMethodError)Use composition instead of inheritance:
functions = Module.new.extend Functionable
Class.new.prepend functions
# Module prepend is disabled. (NoMethodError)You also can’t prepend, only extend:
Module.new.prepend Functionable
# Module prepend is disabled, use extend instead. (NoMethodError)The following is not allowed because you have this behavior when extending Functionable:
Module.new do
extend Functionable
module_function
end
# Module function behavior is disabled. (NoMethodError)Avoid the following since all methods are public by default:
Module.new do
extend Functionable
public
def demo = :demo
end
# Public visibility is disabled. (NoMethodError)Avoid the following since a functionable module isn’t mean to be inherited:
Module.new do
extend Functionable
protected
def demo = :demo
end
# Protected visibility is disabled. (NoMethodError)Avoid the following by using conceal instead:
Module.new do
extend Functionable
private
def demo = :demo
end
# Private visibility is disabled, use conceal instead. (NoMethodError)Avoid aliasing as you are not meant to inherit methods within a functional module:
Module.new do
extend Functionable
def demo = :demo
alias_method :demo, :alt
end
# Aliasing :alt as :demo is disabled. (NoMethodError)Avoid using class variables since they are a code smell and introduce unwanted state:
demo = Module.new do
extend Functionable
def get = class_variable_get :@@bogus
def set = class_variable_set :@@bogus, :bogus
end
demo.get # Getting class variable :@@bogus is disabled. (NoMethodError)
demo.set # Setting class variable :@@bogus is disabled. (NoMethodError)Avoid class methods, use instance methods instead:
Module.new do
extend Functionable
def self.bogus = :bogus
end
# Avoid defining :bogus as a class method because the method will be automatically converted to a class method for you. (NoMethodError)Avoid dynamically setting constants since you can add constants directly to the top of the module:
demo = Module.new do
extend Functionable
def bogus = const_set :BOGUS, :bogus
end
demo.bogus # Setting constant :BOGUS is disabled. (NoMethodError)Avoid dynamically defining a method since you can explicitly define your method instead:
Module.new do
extend Functionable
define_method :bogus, :bogus
end
# Defining method :bogus is disabled. (NoMethodError)Avoid dynamically removing a method since you can explicitly delete the method instead.
Module.new do
extend Functionable
remove_method :bogus
end
# Removing method :bogus is disabled. (NoMethodError)When you lean into the power of functional programming in Ruby, you gain performance and lower your memory footprint since you are creating the minimal amount of objects necessary. In terms of CPU performance, here’s a benchmark script (see bin/benchmark included in this project):
#! /usr/bin/env ruby
# frozen_string_literal: true
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "benchmark-ips"
gem "functionable", path: ".."
end
module ModuleSelf
extend self
def call(message = "benchmark") = message
end
module ModuleFunction
module_function
def call(message = "benchmark") = message
end
module ModuleFunctionable
extend Functionable
def call(message = "benchmark") = message
end
class ClassFunction
def initialize message = "benchmark"
@message = message
end
def call = message
private
attr_reader :message
end
proc_function = proc { |message = "message"| message }
lambda_function = -> message = "benchmark" { message }
memoized_instance = ClassFunction.new
memoized_method = memoized_instance.method :call
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("Proc") { proc_function.call }
benchmark.report("Lambda") { lambda_function.call }
benchmark.report("Module (function)") { ModuleFunction.call }
benchmark.report("Module (functionable)") { ModuleFunctionable.call }
benchmark.report("Module (self)") { ModuleSelf.call }
benchmark.report("Class (new)") { ClassFunction.new.call }
benchmark.report("Class (memoized)") { memoized_instance.call }
benchmark.report("Method (new)") { memoized_instance.method(:call).call }
benchmark.report("Method (memoized)") { memoized_method.call }
benchmark.compare!
endWhen you run the above benchmark, you should see the following results:
ruby 4.0.1 (2026-01-13 revision e04267a14b) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
Proc 2.275M i/100ms
Lambda 2.212M i/100ms
Module (function) 4.654M i/100ms
Module (functionable)
4.682M i/100ms
Module (self) 4.567M i/100ms
Class (new) 2.359M i/100ms
Class (memoized) 4.536M i/100ms
Method (new) 1.176M i/100ms
Method (memoized) 2.173M i/100ms
Calculating -------------------------------------
Proc 23.976M (± 0.1%) i/s (41.71 ns/i) - 120.575M in 5.029071s
Lambda 24.414M (± 0.2%) i/s (40.96 ns/i) - 123.885M in 5.074316s
Module (function) 61.028M (± 0.7%) i/s (16.39 ns/i) - 307.154M in 5.033292s
Module (functionable)
60.864M (± 0.5%) i/s (16.43 ns/i) - 304.327M in 5.000243s
Module (self) 60.261M (± 0.6%) i/s (16.59 ns/i) - 301.446M in 5.002511s
Class (new) 28.202M (± 8.2%) i/s (35.46 ns/i) - 141.526M in 5.048420s
Class (memoized) 57.157M (± 1.0%) i/s (17.50 ns/i) - 285.794M in 5.000639s
Method (new) 13.265M (± 9.7%) i/s (75.39 ns/i) - 65.847M in 5.012654s
Method (memoized) 24.390M (± 0.2%) i/s (41.00 ns/i) - 123.861M in 5.078408s
Comparison:
Module (function): 61027866.3 i/s
Module (functionable): 60864191.4 i/s - same-ish: difference falls within error
Module (self): 60261194.0 i/s - same-ish: difference falls within error
Class (memoized): 57156588.3 i/s - 1.07x slower
Class (new): 28201872.3 i/s - 2.16x slower
Lambda: 24414262.5 i/s - 2.50x slower
Method (memoized): 24389728.5 i/s - 2.50x slower
Proc: 23975703.7 i/s - 2.55x slower
Method (new): 13264797.8 i/s - 4.60x slower
As you can see, a functional module is one of the fastest while everything else is much slower.
To contribute, run:
git clone https://github.com/bkuhlmann/functionable
cd functionable
bin/setupYou can also use the IRB console for direct access to all objects:
bin/console-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.