Skip to content

Instantly share code, notes, and snippets.

@pirj
Last active July 29, 2022 14:19
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pirj/d80f6849f8fdbe4d4123 to your computer and use it in GitHub Desktop.
Save pirj/d80f6849f8fdbe4d4123 to your computer and use it in GitHub Desktop.
Ruby block sugar

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:

@pirj
Copy link
Author

pirj commented Jun 30, 2017

> [1,2,3].map(&:-@)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment