{"id":33916580,"url":"https://github.com/bkuhlmann/functionable","last_synced_at":"2025-12-12T07:24:31.833Z","repository":{"id":322097646,"uuid":"1088243498","full_name":"bkuhlmann/functionable","owner":"bkuhlmann","description":"Enhances modules to be functional by default.","archived":false,"fork":false,"pushed_at":"2025-11-07T23:05:18.000Z","size":25,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-02T02:28:15.617Z","etag":null,"topics":["composition","functional-programming","functions"],"latest_commit_sha":null,"homepage":"https://alchemists.io/projects/functionable","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bkuhlmann.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.adoc","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["bkuhlmann"]}},"created_at":"2025-11-02T15:44:12.000Z","updated_at":"2025-11-08T18:03:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"5922356d-e717-48c4-ba01-507873add74f","html_url":"https://github.com/bkuhlmann/functionable","commit_stats":null,"previous_names":["bkuhlmann/functionable"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/bkuhlmann/functionable","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ffunctionable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ffunctionable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ffunctionable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ffunctionable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bkuhlmann","download_url":"https://codeload.github.com/bkuhlmann/functionable/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ffunctionable/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27678808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-12T02:00:06.775Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composition","functional-programming","functions"],"created_at":"2025-12-12T07:24:28.645Z","updated_at":"2025-12-12T07:24:31.824Z","avatar_url":"https://github.com/bkuhlmann.png","language":"Ruby","readme":":toc: macro\n:toclevels: 5\n:figure-caption!:\n\n:module_function_link: link:https://docs.ruby-lang.org/en/master/Module.html#method-i-module_function[module_function]\n:modules_link: link:https://alchemists.io/articles/ruby_modules[Ruby Modules]\n\n= Functionable\n\nEnhances 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.\n\ntoc::[]\n\n== Features\n\n* Allows modules to be a collection of functional methods.\n* Allows your modules and/or methods to be composable instead of inherited.\n* Provides faster performance and a reduced memory than any object within your application. See the xref:_benchmarks[Benchmarks] section for details.\n\n== Requirements\n\n. link:https://www.ruby-lang.org[Ruby].\n. A solid understanding {modules_link}.\n\n== Setup\n\nTo install _with_ security, run:\n\n[source,bash]\n----\n# 💡 Skip this line if you already have the public certificate installed.\ngem cert --add \u003c(curl --compressed --location https://alchemists.io/gems.pem)\ngem install functionable --trust-policy HighSecurity\n----\n\nTo install _without_ security, run:\n\n[source,bash]\n----\ngem install functionable\n----\n\nYou can also add the gem directly to your project:\n\n[source,bash]\n----\nbundle add functionable\n----\n\nOnce the gem is installed, you only need to require it:\n\n[source,ruby]\n----\nrequire \"functionable\"\n----\n\n== Usage\n\nEnhancing your module to be functional is as simple as extending your module:\n\n[source,ruby]\n----\nmodule Math\n  extend Functionable\n\n  def add(first, second) = first + second\n\n  def subtract(first, second) = first - second\n\n  def multiply(first, second) = first * second\n\n  def divide(first, second) = first / second\nend\n\nMath.add 6, 3       # 9\nMath.subtract 6, 3  # 3\nMath.multiply 6, 3  # 18\nMath.divide 6, 3    # 2\n----\n\nThat's it! Now you can add related methods, as desired, which are specific to your namespace.\n\n=== Conceal\n\nShould you need to make any of your methods private, use the `conceal` method as follows:\n\n[source,ruby]\n----\nmodule Demo\n  extend Functionable\n\n  def welcome(*) = print(*)\n\n  def print(message = \"The default message.\") = puts message\n\n  conceal :print\nend\n\nDemo.welcome  # \"The default message.\"\nDemo.print    # private method 'print' called for module Demo (NoMethodError)\n----\n\nThe `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.\n\n=== Avoidances\n\nFunctional 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.\n\nThis means the following behavior is disabled (each uses an anonymous module and/or class for demonstration purposes and reduced syntax).\n\n==== Extend\n\nUse composition instead of inheritance:\n\n[source,ruby]\n----\nfunctions = Module.new.extend Functionable\nClass.new.extend functions\n\n# Module extend is disabled. (NoMethodError)\n----\n\n==== Include\n\nUse composition instead of inheritance:\n\n[source,ruby]\n----\nfunctions = Module.new.extend Functionable\nClass.new.include functions\n\n# Module include is disabled. (NoMethodError)\n----\n\nYou also can't include `Funtionable`, only extend:\n\n[source,ruby]\n----\nModule.new.include Functionable\n# Module include is disabled, use extend instead. (NoMethodError)\n----\n\n==== Prepend\n\nUse composition instead of inheritance:\n\n[source,ruby]\n----\nfunctions = Module.new.extend Functionable\nClass.new.prepend functions\n\n# Module prepend is disabled. (NoMethodError)\n----\n\nYou also can't prepend, only extend:\n\n[source,ruby]\n----\nModule.new.prepend Functionable\n# Module prepend is disabled, use extend instead. (NoMethodError)\n----\n\n==== Module Function\n\nThe following is not allowed because you have this behavior when extending `Functionable`:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  module_function\nend\n\n# Module function behavior is disabled. (NoMethodError)\n----\n\n==== Public\n\nAvoid the following since all methods are public by default:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  public\n\n  def demo = :demo\nend\n\n# Public visibility is disabled. (NoMethodError)\n----\n\n==== Protected\n\nAvoid the following since a functionable module isn't mean to be inherited:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  protected\n\n  def demo = :demo\nend\n\n# Protected visibility is disabled. (NoMethodError)\n----\n\n==== Private\n\nAvoid the following by using xref:_conceal[conceal] instead:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  private\n\n  def demo = :demo\nend\n\n# Private visibility is disabled, use conceal instead. (NoMethodError)\n----\n\n==== Aliasing\n\nAvoid aliasing as you are not meant to inherit methods within a functional module:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  def demo = :demo\n  alias_method :demo, :alt\nend\n\n# Aliasing :alt as :demo is disabled. (NoMethodError)\n----\n\n==== Class Variables\n\nAvoid using class variables since they are a code smell and introduce unwanted state:\n\n[source,ruby]\n----\ndemo = Module.new do\n  extend Functionable\n\n  def get = class_variable_get :@@bogus\n\n  def set = class_variable_set :@@bogus, :bogus\nend\n\ndemo.get  # Getting class variable :@@bogus is disabled. (NoMethodError)\ndemo.set  # Setting class variable :@@bogus is disabled. (NoMethodError)\n----\n\n==== Class Methods\n\nAvoid class methods, use instance methods instead:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  def self.bogus = :bogus\nend\n\n# Avoid defining :bogus as a class method because the method will be automatically converted to a class method for you. (NoMethodError)\n----\n\n==== Constants\n\nAvoid dynamically setting constants since you can add constants directly to the top of the module:\n\n[source,ruby]\n----\ndemo = Module.new do\n  extend Functionable\n\n  def bogus = const_set :BOGUS, :bogus\nend\n\ndemo.bogus  # Setting constant :BOGUS is disabled. (NoMethodError)\n----\n\n==== Define Method\n\nAvoid dynamically defining a method since you can explicitly define your method instead:\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  define_method :bogus, :bogus\nend\n\n# Defining method :bogus is disabled. (NoMethodError)\n----\n\n==== Remove Method\n\nAvoid dynamically removing a method since you can explicitly delete the method instead.\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  remove_method :bogus\nend\n\n# Removing method :bogus is disabled. (NoMethodError)\n----\n\n==== Undef Method\n\nAvoid dynamically undefining a method since you can explicitly delete the method instead.\n\n[source,ruby]\n----\nModule.new do\n  extend Functionable\n\n  undef_method :bogus\nend\n\n# Undefining method :bogus is disabled. (NoMethodError)\n----\n\n== Benchmarks\n\nWhen 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):\n\n[source,ruby]\n----\n#! /usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/inline\"\n\ngemfile true do\n  source \"https://rubygems.org\"\n\n  gem \"benchmark-ips\"\n  gem \"functionable\", path: \"..\"\nend\n\nmodule ModuleSelf\n  extend self\n\n  def call(message = \"benchmark\") = message\nend\n\nmodule ModuleFunction\n  module_function\n\n  def call(message = \"benchmark\") = message\nend\n\nmodule ModuleFunctionable\n  extend Functionable\n\n  def call(message = \"benchmark\") = message\nend\n\nclass ClassFunction\n  def initialize message = \"benchmark\"\n    @message = message\n  end\n\n  def call = message\n\n  private\n\n  attr_reader :message\nend\n\nproc_function = proc { |message = \"message\"| message }\nlambda_function = -\u003e message = \"benchmark\" { message }\nmemoized_instance = ClassFunction.new\nmemoized_method = memoized_instance.method :call\n\nBenchmark.ips do |benchmark|\n  benchmark.config time: 5, warmup: 2\n\n  benchmark.report(\"Proc\") { proc_function.call }\n  benchmark.report(\"Lambda\") { lambda_function.call }\n  benchmark.report(\"Module (function)\") { ModuleFunction.call }\n  benchmark.report(\"Module (functionable)\") { ModuleFunctionable.call }\n  benchmark.report(\"Module (self)\") { ModuleSelf.call }\n  benchmark.report(\"Class (new)\") { ClassFunction.new.call }\n  benchmark.report(\"Class (memoized)\") { memoized_instance.call }\n  benchmark.report(\"Method (new)\") { memoized_instance.method(:call).call }\n  benchmark.report(\"Method (memoized)\") { memoized_method.call }\n\n  benchmark.compare!\nend\n----\n\nWhen you run the above benchmark, you should see the following results:\n\n----\nruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin24.6.0]\nWarming up --------------------------------------\n                Proc     2.268M i/100ms\n              Lambda     2.327M i/100ms\n   Module (function)     3.984M i/100ms\nModule (functionable)\n                         4.143M i/100ms\n       Module (self)     4.177M i/100ms\n         Class (new)     1.266M i/100ms\n    Class (memoized)     4.128M i/100ms\n        Method (new)   775.573k i/100ms\n   Method (memoized)     2.053M i/100ms\nCalculating -------------------------------------\n                Proc     24.944M (± 1.5%) i/s   (40.09 ns/i) -    124.748M in   5.002287s\n              Lambda     25.683M (± 0.6%) i/s   (38.94 ns/i) -    130.294M in   5.073248s\n   Module (function)     58.738M (± 2.9%) i/s   (17.02 ns/i) -    294.792M in   5.022900s\nModule (functionable)\n                         58.607M (± 2.5%) i/s   (17.06 ns/i) -    294.182M in   5.022770s\n       Module (self)     57.811M (± 2.4%) i/s   (17.30 ns/i) -    292.388M in   5.060572s\n         Class (new)     14.801M (± 1.3%) i/s   (67.56 ns/i) -     74.684M in   5.046650s\n    Class (memoized)     59.130M (± 0.4%) i/s   (16.91 ns/i) -    297.230M in   5.026825s\n        Method (new)      9.030M (± 1.4%) i/s  (110.74 ns/i) -     45.759M in   5.068493s\n   Method (memoized)     24.870M (± 0.2%) i/s   (40.21 ns/i) -    125.231M in   5.035364s\n\nComparison:\n    Class (memoized): 59129953.7 i/s\n   Module (function): 58738432.2 i/s - same-ish: difference falls within error\nModule (functionable): 58607417.9 i/s - same-ish: difference falls within error\n       Module (self): 57810666.8 i/s - same-ish: difference falls within error\n              Lambda: 25683494.8 i/s - 2.30x  slower\n                Proc: 24943941.2 i/s - 2.37x  slower\n   Method (memoized): 24870425.2 i/s - 2.38x  slower\n         Class (new): 14801271.5 i/s - 3.99x  slower\n        Method (new):  9029806.7 i/s - 6.55x  slower\n----\n\nAs you can see, a functional module is one of the fastest while everything else is much slower.\n\n== Development\n\nTo contribute, run:\n\n[source,bash]\n----\ngit clone https://github.com/bkuhlmann/functionable\ncd functionable\nbin/setup\n----\n\nYou can also use the IRB console for direct access to all objects:\n\n[source,bash]\n----\nbin/console\n----\n\n== Tests\n\nTo test, run:\n\n[source,bash]\n----\nbin/rake\n----\n\n== link:https://alchemists.io/policies/license[License]\n\n== link:https://alchemists.io/policies/security[Security]\n\n== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]\n\n== link:https://alchemists.io/policies/contributions[Contributions]\n\n== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]\n\n== link:https://alchemists.io/projects/functionable/versions[Versions]\n\n== link:https://alchemists.io/community[Community]\n\n== Credits\n\n* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].\n* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].\n","funding_links":["https://github.com/sponsors/bkuhlmann"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ffunctionable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbkuhlmann%2Ffunctionable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ffunctionable/lists"}