You won’t find rants on how functional programming improves you, your sanity and your life overall here. There are some examples in the very beginning to save you some time on reading the whole post, just come along if you don’t like how they look like.
By the way, this is not even a blog, so formally this is not even a blog post. This is not a library or a new paradigm. It’s just a few pieces of code that might come handy for your daily job.
Example:
[1, 3.14, -4].map &_.safe{ magnitude odd? } # => [true, nil, false]
Starting point:
[ActiveRecord, ActiveSupport].flat_map { |klass| klass.name.snake_case.split }
# => ["active", "record", "active", "support"]
"Functional" style:
[ActiveRecord, ActiveSupport].map(&:name).map(&:snake_case).flat_map(&:split)
Magic behind &:name syntax is that an ampersand operator calls .to\_proc on everything that is not a Proc yet, in this case on a Symbol. And Symbol’s to\_proc implementation would look like that in Ruby:
class Symbol
def to_proc
-> argument { argument.send self } # Where self is :odd?
end
end
but how about that?
[ActiveRecord, ActiveSupport].flat_map(&[:name, :snake_case, :split])
Easily, Array is no different from Symbol that just already has .to_proc defined, let’s add it to Array too:
class Array
def to_proc
-> (arg) {
inject(arg) { |arg, symbol| symbol.to_proc.call(arg) }
}
end
end
Might be more readable this way:
class Array
def to_proc
-> (arg) {
map(&:to_proc).inject(arg) { |arg, symbol| symbol.call(arg) }
}
end
end
It takes elements of an array, symbols, one by one, converts them to_proc and calls them, chaining, e.g. calling the next method on the result of previous computation.
Might be even easier to understand if we adopt implementation from Symbol rather than use it:
class Array
def to_proc
-> (arg) {
inject(arg) { |arg, symbol| argument.send symbol }
}
end
end
Remember that syntax to create an array of symbols without the need for commas and colons?
%i( a b c d ) # => [:a, :b, :c, :d]
We then may transform our code like so:
[ActiveRecord, ActiveSupport].map(&%i( name titlecase split ))
[12.214, Math::PI, 10, 2394091284912808, -1].map(&%i( round magnitude )).select(&%i( prime? over9000? ))
But that (&%i( … )) is ugly, so how about that:
[-1, -2, -3].map &_{ magnitude }
You have seen that already if you’re using Sequel ORM or Squeel ActiveRecord Sugar.
Easily doable. Let’s start with underscore thing. It may sound weird, but underscore is a function that takes a block and returns an array that then is asked to return a Proc. This brings us one step closer to building code using our code. Less talk, more action:
module Kernel
def _ &block
block
end
end
[1, 2, 3].map &_{ |x| x * 2 } # => [2, 4, 6]
Underscore is littering all objects, later we’re hide all those methods using mysterious Refinements.
class ProcEx
def initialize &block
@chain = []
instance_exec &block
end
def to_proc @chain.to_proc end
def method_missing method_name, *_args
@chain.unshift method_name
end
end
module Kernel
def _ &block
ProcEx.new &block
end
end
Now it works like so:
[-1, -2, -3].map &_{ magnitude odd? }
|
Note
|
You may ask why is that we need an ampersand and an underscore, since Squeel and Sequel don’t require anything like that? They both operate on their own methods, while we support any method that accepts a block. We may add a method_missing to an Object and catch all method calls for (inexistent) methods which name ends with _ and define them, proxying the call to original method not suffixed with an underscore, but with unquoted block (e.g. { magnitude odd? } transformed to { |object| object.magnitude.odd? } and use it like [1, 2].map { odd? }, but in this case it’s not that easy to add block modifiers (safe et c).
|
It is even easier to inherit from Array instead of object composition (not to be messed up with functional composition, more on that later), and to be able to use some methods already defined in an Array, more on that later.
class Chain < Array
def initialize &block
instance_exec &block
end
def to_proc
-> (arg) {
inject(arg) { |arg, symbol| symbol.to_proc.call(arg) }
}
end
def method_missing method_name, *_args
unshift method_name
end
end
module Kernel
def _ &block
Chain.new &block
end
end
|
Note
|
An attentive reader might have noticed that we’re calling 'unshift', that prepends to an array instead of adding to its end with 'push' or '<<'. There’s a reason for that. This is due to the fact that Ruby evaluates methods in a block that we pass to it in reverse order, e.g. a rightmost 'odd?' is called first, and its "return value" is passed as an argument to the function to the left of it, 'magnitude', and so on. But we’re going to execute them in a different way, the way we have written them, left to right, this is why we 'unshift'. |
Okay, okay, got it. Where are we already in terms of a nice syntax? Ruby classic:
[-1, -2, -3].map { |number| number.magnitude.odd? }
[-1, -2, -3].map(&:magnitude).map(&:odd?)
Our hackery:
[-1, -2, -3].map &%i( magnitude odd? )
[-1, -2, -3].map &_{ magnitude odd? }
Syntax with an underscore and block is a bit more convenient, since we may add some modifiers changing the way how the insides of the block are being parsed, and add some interesting behavior, e.g.:
[1, 3.1415, 2].map &_.safe{ odd? } # => [true, false]
This will make sure that you’re safe to call odd? on a Float, it will be skipped.
Guess what? We have come to a simpler safe navigation.
Original:
comment.try(:article).try(:author).try(:name)
Ruby 2.3:
comment&.article&.author&.name
You’re not going to advocate for 'tell, do not ask' hell, right?
comment.article_author_name
How about that?
comment.itself &_.safe{ article author name }
Right, too long. But it pays out if the call chain is long enough.
class Chain < Array
def initialize &block
instance_exec &block
end
def to_proc
-> (arg) {
inject(arg) { |arg, symbol| symbol.to_proc.call(arg) }
}
end
def method_missing method, *_args
unshift method
end
end
class SafeChain < Chain
def to_proc
-> (arg) {
inject(arg) { |arg, symbol| symbol.to_proc.call(arg) rescue nil }
}
end
end
class Underscored
def safe &block
SafeChain.new &block
end
end
module Kernel
def _
if block_given?
Chain.new Proc.new
else
Underscored.new
end
end
end
Yihaa!:
p [1, 3.14, -4].map &_.safe{ magnitude odd? } # => [true, nil, false]
This is for the itself thing:
class Object
alias _itself itself
def itself &block
if block_given?
yield _itself
else
_itself
end
end
end
Comment = Struct.new :article Article = Struct.new :author Author = Struct.new :name
comment = Comment.new Article.new Author.new 'Stephen King' dumb_comment = Comment.new Article.new nil
Trying:
comment.itself &_.safe{ article author name } # => "Stephen King"
dumb_comment.itself &_.safe{ article author name } # => nil
Yay! That’s it for today!
Homework:
Implement this as a Refinement not to litter Object and Kernel’s class hierarchy.
Next up: how about passing some arguments?
['Egor Letov', 'Oleg Oparin'].flat_map &_{ split join["\n"] }
or
['EgorLetov', 'OlegOparin'].map &_{ scan(/[A-Z][^A-Z]*/) join('_') downcase }
Should be possible with currying.
|
Note
|
It’s unfortunate, but due to Ruby’s belief that > < >= ⇐ == + * are Binary operators, while ~ and ! are unary we’re limited and this kind of syntaxes won’t work: |
[1, 2, 3].inject &_{ + }
[1, 10, 100].select &_{ > 10 }
|
Note
|
- and + have modifications that are used for both unary and binary |
|
Note
|
* and & are also used as unary operators, but you cannot override their behaviour More here: http://www.zenspider.com/Languages/Ruby/QuickRef.html#4 |
Articles worth reading:
http://www.rubyinside.com/rubys-unary-operators-and-how-to-redefine-their-functionality-5610.html http://ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby/ http://blog.jessitron.com/2013/03/passing-functions-in-ruby-harder-than.html https://www.omniref.com/ruby/2.2.0/symbols/Proc/yield?#annotation=4087638&line=711&hn=1 http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html