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