Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

119 lines (72 sloc) 8.742 kb

String to Block

String to Block is a port of the String Lambdas from Oliver Steele's Functional Javascript library. I have modified the syntax to hew closer to Ruby's idioms.

String to Block is based on String#to_proc. The difference is that String to Proc accomplishes its magic by opening up String and adding a to_proc method, then calling that method twice every time you write something like:

(1..100).map(&'1..n').inject(&'+')

String to Block does something entirely different. It rewrites (1..100).map(&'1.._').map(&'+') as:

(1..num).map { |_| (1.._) }.inject { |_0, _1| (_0 + _1) }

This is obviously much faster at run time and more importantly, does not cause a conflict if somewhere else in your application you write your own to_proc method for String. And when I say "you," I mean you and the authors of every gem and plugin you use. For example, Sami Samhuri.

gives

String to Block provides several key abbreviations: First, -> syntax for blocks in Ruby 1.8. So instead of (1..100).inject { |x,y| x + y }, you can write (1..100).inject(&'x,y -> x + y'). I read this out loud as "x and y gives x plus y."If the -> seems foreign, it is only because -> is in keeping with modern functional languages and mathematical notation.

Gives isn't a particularly big deal considering how easy it is to write an old-fashioned block, it's a lot more handy when doing really functional things. But it's included so that you can make code that's aristocratic.

inferred parameters

Second, String to Block adds inferred parameters: If you do not use ->, String to Block attempts to infer the parameters. So if you write 'x + y', String to Block rewrites it as { |x,y| x + y }. There are certain expressions where this doesn't work, and you have to use ->, but for really simple cases it works just fine. And frankly, for really simple cases you don't need the extra scaffolding.

Here're some examples using inferred parameters:

foo.select(&'x.kind_of?(Numeric)')
  => foo.select { |x| x.kind_of?(Numeric) }

bar.map(&'x ** 2')
  => bar.map { |x| x ** 2 }

I have good news and bad news about inferred parameters and String to Block in general. It uses regular expressions to do its thing, which means that complicated things often don't work. For example, nesting -> only works when writing functions that return functions. So 'x -> y -> x + y' is a function that takes an x and returns a function that takes a y and returns x + y. That works. But 'z -> z.inject(&"sum, n -> sum + n")' does NOT work.

I considered fixing this with more sophisticated parsing, however the simple truth is this: String to Block is not a replacement for blocks, it's a tool to be used when what you're doing is so simple that a block is overkill. If String to Block doesn't work for something, it probably isn't ridiculously simple any more.

it

The third abbreviation is a special case. If there is only one parameter, you can use _ (the underscore) without naming it. This is pronounced "it," because it works like an anaphor in human language. If you use "it," then String to Block doesn't try to infer any more parameters, so this can help you write things like:

foo.select(&'_')
  => foo.select { |_| _ }

bar.map(&'_.inject { |sum, n| sum + n }')
  => bar.map { |_| _.inject { |sum, n| sum + n } }

Anaphora in Ruby discusses this idea in greater detail. Also, the Block Anaphora rewriter implements this version of "it" without converting a string to a block.

point-free

String to Block has a fourth and even more extreme abbreviation up its sleeve, point-free style: "Function points" are what functional programmers usually call parameters. Point-free style consists of describing how functions are composed together rather than describing what happens with their arguments. So, let's say that I want a function that combines .inject with +. One way to say that is to say that I want a new function that takes its argument and applies an inject to it, and the inject takes another function with two arguments and applies a + to them:

foo.map { |z| z.inject { |sum, n| sum + n } }

The other way is to say that I want to compose .inject and + together. Without getting into a compose function like Haskell's . operator, String to Block has enough magic to let us write the above as:

foo.map(&".inject(&'+')")
  => foo.map { |_0| _0.inject { |_0, _1| _0 + _1 } }

Meaning "I want a block that does an inject using plus." Point-free style does require a new way of thinking about some things, but it is a clear win for simple cases. Proof positive of this is the fact that Ruby on Rails and Ruby 1.9 have both embraced point-free style with Symbol#to_proc. That's exactly how (1..100).inject(&:+) works!

String to Block supports fairly simple cases where you are sending a message or using a binary operator. So if we wanted to go all out, we could write things like:

foo.reject(&'.kind_of?(Numeric)')
  => foo.reject { |_0| _0.kind_of?(Numeric) }

bar.map(&'** 2')
  => bar.map { |_0| _0 ** 2 }

blitzes.map(&".inject(&'+')")
  => blitzes.map { |_0| _0.inject { |_0, _1| _0 + _1 } }

There's no point-free magic for the identity function, although this example tempts me to special case the empty string!

Blocks not Procs

Unlike String#to_proc, String to Block is strictly for blocks. If you want a lambda, write lambda(&'x,y -> x + y').

When should we use all these tricks?

String to Block provides these options so that you as a programmer can choose your level of ceremony around writing blocks. But of course, you have to use the tool wisely. My personal rules of thumb are:

  1. Embrace inferred parameters for well-known mathematical or logical operations. For these operations, descriptive parameter names are usually superfluous. Follow the well-known standard and use x, y, z, and w; or a, b and c; or n, i, j, and k for the parameters. If whatever it is makes no sense using those variable names, don't used inferred parameters.
  2. Embrace the hole for extremely simple one-parameter lambdas that aren't intrinsically mathematical or logical such as methods that use .method_name and for the identity function.
  3. Embrace point-free style for methods that look like operators.
  4. Embrace -> notation for extremely simple cases where I want to give the parameters a descriptive name.
  5. Use ordinary Ruby blocks for everything else.

How does String to Block differ from String#to_proc?

First, RewriteRails preprocesses your Ruby code before the file is read. Therefore, you only pay the cost of converting a String to a Block once, not every time the code is evaluated. This is a very big performance optimization. Second, String to Block only works with literal strings. It isn't possible to construct a string at run time and then convert it to a block with &.

Third (and this is a subtle point), String#to_proc worked by using eval in the global environment, therefore the procs it produced were not closures in the ordinary sense of the word. For example, with String#to_proc:

require 'string_to_proc'

increase = 1
(1..10).map(&'+ increase')
  # => NameError: undefined local variable or method ‘increase’ for "+ increase":String

increase is not defined in the global environment, so thing do not work as you might expect. However, let's try that some code again using String to Block:

increase = 1
(1..10).map(&'+ increase')
  # => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

This works because RewriteRails doesn't construct a string and ask Ruby to eval it globally at run time, it converts the code in the file and let's Ruby's interpreter determine how to treat increase.


RewriteRails

Jump to Line
Something went wrong with that request. Please try again.