https://github.com/bkuhlmann/functionable
Enhances modules to be functional by default.
https://github.com/bkuhlmann/functionable
composition functional-programming functions
Last synced: about 1 month ago
JSON representation
Enhances modules to be functional by default.
- Host: GitHub
- URL: https://github.com/bkuhlmann/functionable
- Owner: bkuhlmann
- License: other
- Created: 2025-11-02T15:44:12.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2025-11-07T23:05:18.000Z (2 months ago)
- Last Synced: 2025-12-02T02:28:15.617Z (about 1 month ago)
- Topics: composition, functional-programming, functions
- Language: Ruby
- Homepage: https://alchemists.io/projects/functionable
- Size: 24.4 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.adoc
- Funding: .github/FUNDING.yml
- License: LICENSE.adoc
- Citation: CITATION.cff
Awesome Lists containing this project
README
:toc: macro
:toclevels: 5
:figure-caption!:
:module_function_link: link:https://docs.ruby-lang.org/en/master/Module.html#method-i-module_function[module_function]
:modules_link: link:https://alchemists.io/articles/ruby_modules[Ruby Modules]
= Functionable
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_link} by preventing inheritance and reducing the memory overhead due to making copies of the original methods.
toc::[]
== Features
* 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 xref:_benchmarks[Benchmarks] section for details.
== Requirements
. link:https://www.ruby-lang.org[Ruby].
. A solid understanding {modules_link}.
== Setup
To install _with_ security, run:
[source,bash]
----
# 💡 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 HighSecurity
----
To install _without_ security, run:
[source,bash]
----
gem install functionable
----
You can also add the gem directly to your project:
[source,bash]
----
bundle add functionable
----
Once the gem is installed, you only need to require it:
[source,ruby]
----
require "functionable"
----
== Usage
Enhancing your module to be functional is as simple as extending your module:
[source,ruby]
----
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 # 2
----
That's it! Now you can add related methods, as desired, which are specific to your namespace.
=== Conceal
Should you need to make any of your methods private, use the `conceal` method as follows:
[source,ruby]
----
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 link:https://docs.ruby-lang.org/en/master/Module.html#method-i-private_class_method[private_class_method] which means you can use a string, symbol, a single argument, multiple arguments, or an array.
=== Avoidances
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).
==== Extend
Use composition instead of inheritance:
[source,ruby]
----
functions = Module.new.extend Functionable
Class.new.extend functions
# Module extend is disabled. (NoMethodError)
----
==== Include
Use composition instead of inheritance:
[source,ruby]
----
functions = Module.new.extend Functionable
Class.new.include functions
# Module include is disabled. (NoMethodError)
----
You also can't include `Funtionable`, only extend:
[source,ruby]
----
Module.new.include Functionable
# Module include is disabled, use extend instead. (NoMethodError)
----
==== Prepend
Use composition instead of inheritance:
[source,ruby]
----
functions = Module.new.extend Functionable
Class.new.prepend functions
# Module prepend is disabled. (NoMethodError)
----
You also can't prepend, only extend:
[source,ruby]
----
Module.new.prepend Functionable
# Module prepend is disabled, use extend instead. (NoMethodError)
----
==== Module Function
The following is not allowed because you have this behavior when extending `Functionable`:
[source,ruby]
----
Module.new do
extend Functionable
module_function
end
# Module function behavior is disabled. (NoMethodError)
----
==== Public
Avoid the following since all methods are public by default:
[source,ruby]
----
Module.new do
extend Functionable
public
def demo = :demo
end
# Public visibility is disabled. (NoMethodError)
----
==== Protected
Avoid the following since a functionable module isn't mean to be inherited:
[source,ruby]
----
Module.new do
extend Functionable
protected
def demo = :demo
end
# Protected visibility is disabled. (NoMethodError)
----
==== Private
Avoid the following by using xref:_conceal[conceal] instead:
[source,ruby]
----
Module.new do
extend Functionable
private
def demo = :demo
end
# Private visibility is disabled, use conceal instead. (NoMethodError)
----
==== Aliasing
Avoid aliasing as you are not meant to inherit methods within a functional module:
[source,ruby]
----
Module.new do
extend Functionable
def demo = :demo
alias_method :demo, :alt
end
# Aliasing :alt as :demo is disabled. (NoMethodError)
----
==== Class Variables
Avoid using class variables since they are a code smell and introduce unwanted state:
[source,ruby]
----
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)
----
==== Class Methods
Avoid class methods, use instance methods instead:
[source,ruby]
----
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)
----
==== Constants
Avoid dynamically setting constants since you can add constants directly to the top of the module:
[source,ruby]
----
demo = Module.new do
extend Functionable
def bogus = const_set :BOGUS, :bogus
end
demo.bogus # Setting constant :BOGUS is disabled. (NoMethodError)
----
==== Define Method
Avoid dynamically defining a method since you can explicitly define your method instead:
[source,ruby]
----
Module.new do
extend Functionable
define_method :bogus, :bogus
end
# Defining method :bogus is disabled. (NoMethodError)
----
==== Remove Method
Avoid dynamically removing a method since you can explicitly delete the method instead.
[source,ruby]
----
Module.new do
extend Functionable
remove_method :bogus
end
# Removing method :bogus is disabled. (NoMethodError)
----
==== Undef Method
Avoid dynamically undefining a method since you can explicitly delete the method instead.
[source,ruby]
----
Module.new do
extend Functionable
undef_method :bogus
end
# Undefining method :bogus is disabled. (NoMethodError)
----
== Benchmarks
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):
[source,ruby]
----
#! /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!
end
----
When you run the above benchmark, you should see the following results:
----
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin24.6.0]
Warming up --------------------------------------
Proc 2.268M i/100ms
Lambda 2.327M i/100ms
Module (function) 3.984M i/100ms
Module (functionable)
4.143M i/100ms
Module (self) 4.177M i/100ms
Class (new) 1.266M i/100ms
Class (memoized) 4.128M i/100ms
Method (new) 775.573k i/100ms
Method (memoized) 2.053M i/100ms
Calculating -------------------------------------
Proc 24.944M (± 1.5%) i/s (40.09 ns/i) - 124.748M in 5.002287s
Lambda 25.683M (± 0.6%) i/s (38.94 ns/i) - 130.294M in 5.073248s
Module (function) 58.738M (± 2.9%) i/s (17.02 ns/i) - 294.792M in 5.022900s
Module (functionable)
58.607M (± 2.5%) i/s (17.06 ns/i) - 294.182M in 5.022770s
Module (self) 57.811M (± 2.4%) i/s (17.30 ns/i) - 292.388M in 5.060572s
Class (new) 14.801M (± 1.3%) i/s (67.56 ns/i) - 74.684M in 5.046650s
Class (memoized) 59.130M (± 0.4%) i/s (16.91 ns/i) - 297.230M in 5.026825s
Method (new) 9.030M (± 1.4%) i/s (110.74 ns/i) - 45.759M in 5.068493s
Method (memoized) 24.870M (± 0.2%) i/s (40.21 ns/i) - 125.231M in 5.035364s
Comparison:
Class (memoized): 59129953.7 i/s
Module (function): 58738432.2 i/s - same-ish: difference falls within error
Module (functionable): 58607417.9 i/s - same-ish: difference falls within error
Module (self): 57810666.8 i/s - same-ish: difference falls within error
Lambda: 25683494.8 i/s - 2.30x slower
Proc: 24943941.2 i/s - 2.37x slower
Method (memoized): 24870425.2 i/s - 2.38x slower
Class (new): 14801271.5 i/s - 3.99x slower
Method (new): 9029806.7 i/s - 6.55x slower
----
As you can see, a functional module is one of the fastest while everything else is much slower.
== Development
To contribute, run:
[source,bash]
----
git clone https://github.com/bkuhlmann/functionable
cd functionable
bin/setup
----
You can also use the IRB console for direct access to all objects:
[source,bash]
----
bin/console
----
== Tests
To test, run:
[source,bash]
----
bin/rake
----
== link:https://alchemists.io/policies/license[License]
== link:https://alchemists.io/policies/security[Security]
== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
== link:https://alchemists.io/policies/contributions[Contributions]
== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]
== link:https://alchemists.io/projects/functionable/versions[Versions]
== link:https://alchemists.io/community[Community]
== Credits
* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].