An experimental toolbox for writing "functional" Ruby.
Add this line to your application's Gemfile:
gem 'fun-ruby', github: 'woarewe/fun-ruby', branch: 'main'
And then execute:
bundle install
In 2020 I started looking into functional programming and got really excited about having a Ruby library that would make programming in Ruby feel as much close as possible to using "pure" functional languages.
A function will be called only after all the required arguments are passed. Until that the function will be returning a wrapper expecting the rest of arguments.
# F::Modules::Hash.fetch requires two arguments:
# - a key
# - a hash
# Calling a function without any argument returns
# a function expecting the rest arguments to be applied
# either one by one or altogether at once
f = F::Modules::Hash.fetch
f.(:number, { number: 3 }) # => 3
f.(:number).({ number: 3 }) # => 3
For better understanding I strongly recommend to watch this video.
Given that functions are curried, changing function signatures in order to have the params that change more rarely first and the params that change more often last allows us to create new more specific functions from more generic ones.
rails = { stars: 52_300 }
f_ruby = { stars: 3 }
repos = [rails, f_ruby]
# Creating a function that gets a number stars
stars = F::Modules::Hash.fetch(:stars)
stars.(rails) # => 52_300
stars.(f_ruby) # => 3
# Creating a function that gets a number of stars for an array
stars_map = F::Modules::Enum.map(get_stars)
stars_map.(repos) #=> [52_300, 3]
However, there are situations when data remains the same
but the applicable actions differ. In such case there is
a placeholder F._
that can help to achieve the desired behavior.
rails = { stars: 52_300, forks: 10_000 }
fetch_from_rails = F::Modules::Hash.fetch(F._, rails)
fetch_from_rails.(:stars) # => 52_300
fetch_from_rails.(:forks) # => 10_000
The library goes with a set of utils that will help you achieve your goals in an elegant way. For example, the pipe function:
repos = [
{ name: 'Rails', stars: 52_300 },
{ name: 'FunRuby', stars: 3 }
]
label = F::Modules::Function.pipe(
F::Modules::Hash.values_at([:name, :stars]), # returns pairs of [name, stars]
F::Modules::Array.join(" -> "), # joins the pairs
)
labels = F::Modules::Enum.map(label)
labels.(repos) # => ["Rails -> 52300", "FunRuby -> 3"]
Or the curry function that will make the functions from your codebase fully compatible with the library utils.
# A function from your codebase
def join_three_values(first, second, third)
[first, second, third].join(' ')
end
a, b, c = ["A", "B", "C"]
join_three_values(a, b, c) #=> "A B C"
# Making it curried...
curried = F::Modules::Function.curry(method(:join_three_values))
# Using it as a library function
curried.(a).(b).(c) # => "A B C"
# It evens supports placeholders
paste_in_the_middle = curried.("Beginning", F._, "End")
paste_in_the_middle.("I'm in the middle") # => "Beginning I'm in the middle End"
Let's say we've got a pair of functions and we would like to reuse them across the different parts of our application. Since local variables are not exportable to other files the ways we can share the functions are:
- Assigning them to constants
- Assigning them to global variables
- Returning them from class singleton methods
But none of the approaches are really needed because the library comes with a container where you can define. Here is an example:
# definition.rb
F.define do
f(:repo_stars) { F::Modules::Hash.fetch(:stars) }
f(:repo_name) { F::Modules::Hash.fetch(:name) }
end
# another_file.rb
repo = { stars: 33, name: 'FunRuby'}
F.container.fetch(:repo_name).(repo) # => "FunRuby"
F.container.fetch(:repo_stars).(repo) # => 33
What? Function names have the repetitive part repo? Let's puts them under a namespace!
# definition.rb
F.define do
namespace :repo do
f(:stars) { F::Modules::Hash.fetch(:stars) }
f(:name) { F::Modules::Hash.fetch(:stars) }
end
end
# another_file.rb
repo = { stars: 33, name: 'FunRuby'}
F.container.fetch("repo.name").(repo) # => "FunRuby"
F.container.fetch("repo.stars").(repo) # => 33
But what I we want to use the functions inside classes or modules? The way we access the function and call it seems too long and inconvenient. This is not a problem either because the container can be imported.
# definition.rb
F.define do
namespace :repo do
f(:stars) { F::Modules::Hash.fetch(:stars) }
f(:name) { F::Modules::Hash.fetch(:name) }
end
namespace :hello do
f(:word) { -> { puts "Hello, world" } }
end
end
# another_file.rb
class Feature
include F.import(:repo, :hello)
def execute(repo)
puts f(:stars).(repo)
puts f(:name).(repo)
puts f(:word).()
end
end
repo = { stars: 33, name: 'FunRuby'}
Feature.new.execute(repo)
Sometimes it also happens that the definition of a function is deeply nested or there are two functions or two namespaces that have the same name and we want them both to be present in the same scope. The container imports support aliasing. You can alias both a final function and a namespace.
F.define do
namespace :foo do
namespace :buzz do
namespace :bar do
f(:hello) { -> { puts "Hello from Bar!" } }
f(:goodbye) { -> { puts "Goodbye from Bar!" } }
end
end
end
namespace :green do
namespace :red do
namespace :blue do
f(:hello) { -> { puts "Hello from Blue!" } }
f(:goodbye) { -> { puts "Goodbye from Blue!" } }
end
end
end
end
class Feature
include F.import(
'foo.buzz.bar' => 'top',
'green.red.blue' => 'bottom',
'foo.buzz.bar.goodbye' => 'top_bye',
'green.red.blue.goodbye' => 'bottom_bye'
)
def execute
puts f('top.hello').()
puts f('bottom.hello').()
puts f('top_bye').()
puts f('bottom_bye').()
end
end
All the functions have RubyDoc with examples that are run as tests on CI. So, they are just working!
API Docs.
In progress...
The library is aimed to cover most of th methods the core ruby classes have.
- Hey Lodash, you're doing it wrong!
- Ramda JS
- Ramda JS Ruby Port
- Lambda Calculus - Fundamentals of Lambda Calculus & Functional Programming in JavaScript
- Why Isn't Functional Programming the Norm? – Richard Feldman
All kinds of contributions are welcome!
In case you don't really know where to start, just read the details down below 😉
-
All the implemented modules are located under
lib/fun_ruby/modules
. Most of the modules have the names matching correspondent Ruby Core classes/modules. For example, if you are looking for the entry point ofArray
you will find it atlib/fun_ruby/modules/array.rb
-
A module entry point file contains only the module definition and a method being used to coerce an input to the object that the module works with.
-
Each implemented function has its own file. For instance
Array#size
is located underlib/fun_ruby/modules/array/size.rb
The library goes with a useful command line interface that will help you to add new modules and functions without typing much boilerplate.
Adding a new module
bin/generate module <MODULE_NAME>
Adding a new function to a module
# TODO: Will be added soon