|
# convenience method to print the code and the output |
|
def pc(arg, offset = 0) |
|
file, line = caller.first.split(":") |
|
actual_line = line.to_i - 1 + offset |
|
code = File.read(file).split("\n")[actual_line].strip.sub("pc ", "") |
|
result = arg.is_a?(String) ? arg : arg.inspect |
|
puts "Line ##{actual_line}:" |
|
puts " #{[code, result].join(" # => ")}" |
|
puts |
|
end |
|
|
|
# Let's start with a very simple lambda: |
|
|
|
PrintsParameters = ->(a, b, c, d, e) { |
|
[a, b, c, d, e] |
|
} |
|
|
|
# This lambda takes 5 parameters, and turns them into an array. |
|
# |
|
# You can call PrintsParameters 4 different ways: |
|
|
|
pc PrintsParameters.yield(1, 2, 3, 4, 5) |
|
pc PrintsParameters.call(1, 2, 3, 4, 5) |
|
pc PrintsParameters.(1, 2, 3, 4, 5) |
|
pc PrintsParameters[1, 2, 3, 4, 5] |
|
|
|
# For the rest of this document, I'll use `.call`. |
|
# |
|
# Imagine you have some of code that looks like this: |
|
|
|
PrintsParameters.call('Firsties!', 2, 3, 4, 5) |
|
PrintsParameters.call('Firsties!', :b, :c, :d, :e) |
|
PrintsParameters.call('Firsties!', 'b', 'c', 'd', 'e') |
|
|
|
# That is, for some part of your code 'Firsties!' will always be passed as the |
|
# first argument. You see that this code is not dry, and you want to create a |
|
# new lambda where 'Firsties!' always comes first. |
|
# |
|
# That's pretty easy - you just write something like this: |
|
|
|
PrintsFirsties = ->(b, c, d, e) { |
|
PrintsParameters.call('Firsties!', b, c, d, e) |
|
} |
|
|
|
# Then you change your code to this: |
|
|
|
PrintsFirsties.call(2, 3, 4, 5) |
|
PrintsFirsties.call(:b, :c, :d, :e) |
|
PrintsFirsties.call('b', 'c', 'd', 'e') |
|
|
|
# So far so good, you DRYd your code up a little. |
|
# Then you go to another place, and you see this: |
|
|
|
PrintsParameters.call('Secondz', 2, 3, 4, 5) |
|
PrintsParameters.call('Secondz', :b, :c, :d, :e) |
|
PrintsParameters.call('Secondz', 'b', 'c', 'd', 'e') |
|
|
|
# So you create another lambda like this: |
|
|
|
PrintsSecondz = ->(b, c, d, e) { |
|
PrintsParameters.call('Secondz!', b, c, d, e) |
|
} |
|
|
|
# Now let's say you see this happen a few other times in your codebase. |
|
# You want to dry that up a bit, so you create a higher-order lambda, one that |
|
# returns another lambda. Kind of like a lambda factory: |
|
|
|
CreatesPrintLambda = ->(a) { |
|
->(b, c, d, e) { |
|
PrintsParameters.call(a, b, c, d, e) |
|
} |
|
} |
|
|
|
# Now you can create as many of these specialized versions |
|
# of PrintsParameters as you like: |
|
|
|
PrintsTurtlz = CreatesPrintLambda.call('Turtlz') |
|
pc PrintsTurtlz.call(2, 3, 4, 5) |
|
|
|
# Then you come across some more code in your site that looks like this: |
|
|
|
PrintsParameters.call('Hip', 'Cat', 3, 4, 5) |
|
PrintsParameters.call('Hip', 'Cat', :c, :d, :e) |
|
PrintsParameters.call('Hip', 'Cat', 'c', 'd', 'e') |
|
|
|
# What! Now you have the same problem, but with both the first _and_ second |
|
# parameter to PrintsParameters. |
|
|
|
# You could use that same technique to just create a wrapper lambda, |
|
# and then another wrapper lambda, like so: |
|
|
|
PrintsHipCats = ->(a, b) { |
|
->(c, d, e) { |
|
PrintsParameters.call(a, b, c, d, e) |
|
} |
|
} |
|
# But now what happens when you want a lambda that just hard-codes |
|
# 'Hip', but not 'Cats'? Lambda-splosion! That's what. |
|
|
|
# So you set out to create a function that will allow for much easier |
|
# cascading of defaults. It turns out this isn't super-easy. |
|
# |
|
# Your first crack at it might look like this: |
|
|
|
NestsAllAndCallsLast = ->(*args1) { |
|
if args1.length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*args1) |
|
elsif args1.length > PrintsParameters.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*args2) { |
|
if (args1 + args2).length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*(args1 + args2)) |
|
elsif (args1 + args2).length > PrintsParameters.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*args3) { |
|
if (args1 + args2 + args3).length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*(args1 + args2 + args3)) |
|
elsif (args1 + args2 + args3).length == PrintsParameters.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*args4) { |
|
if (args1 + args2 + args3 + args4).length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*(args1 + args2 + args3 + args4)) |
|
elsif (args1 + args2 + args3 + args4).length == PrintsParameters.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*args5) { |
|
if (args1 + args2 + args3 + args4 + args5).length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*(args1 + args2 + args3 + args4 + args5)) |
|
else |
|
raise ArgumentError |
|
end |
|
} |
|
end |
|
} |
|
end |
|
} |
|
end |
|
} |
|
end |
|
} |
|
|
|
# That is super gnarly, but does the job. Now you can write things like: |
|
|
|
pc NestsAllAndCallsLast.call('Turtlz', 'Turtlz', 'Turtlz', 'Turtlz', 'Turtlz') |
|
pc NestsAllAndCallsLast.call('Turtlz', 'Turtlz', 'Turtlz', 'Turtlz').call(5) |
|
pc NestsAllAndCallsLast.call('Turtlz', 'Turtlz').call('Turtlz', 'Turtlz').call(5) |
|
pc NestsAllAndCallsLast.call('Turtlz', 'Turtlz', 'Turtlz').call(4, 5) |
|
pc NestsAllAndCallsLast.call('Turtlz', 'Turtlz').call(3, 4, 5) |
|
pc NestsAllAndCallsLast.call('Turtlz').call(2, 3, 4, 5) |
|
|
|
# and... |
|
|
|
DoubleTurtlz = NestsAllAndCallsLast.call('Turtlz', 'Turtlz') |
|
TripleTurtlz = DoubleTurtlz.call('Turtlz') |
|
pc TripleTurtlz.call(4, 5) |
|
|
|
# But this solution still has a few problems: |
|
# - if you change the arity of PrintsParameters, your NestsAllAndCallsLast |
|
# lambda will need another branch (no recursion) |
|
# - if you want to do this for another lambda, you have to write it over again |
|
# or figure out some way to generalize it |
|
|
|
# So to solve the first problem (no recursion), you can create a recursive proc that will |
|
# do the same thing: |
|
|
|
RecursiveLambda = ->(*args) { |
|
if args.length == PrintsParameters.parameters.length |
|
PrintsParameters.call(*args) |
|
elsif args.length > PrintsParameters.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*inner_args) { |
|
RecursiveLambda.(*(args + inner_args)) |
|
} |
|
end |
|
} |
|
|
|
pc RecursiveLambda.(1, 2, 3).(4, 5) |
|
pc RecursiveLambda.(1) |
|
pc RecursiveLambda.(1).(2) |
|
pc RecursiveLambda.(1).(2).(3) |
|
pc RecursiveLambda.(1).(2).(3).(4) |
|
pc RecursiveLambda.(1).(2).(3).(4).(5) |
|
|
|
# But this still has the problem that it's hard-coded to PrintsParameters. So |
|
# you need to be able to generate that lambda: |
|
|
|
Curry = ->(proc){ |
|
inner = ->(*args) { |
|
if args.length == proc.parameters.length |
|
proc.call(*args) |
|
elsif args.length > proc.parameters.length |
|
raise ArgumentError |
|
else |
|
->(*inner_args) { |
|
args += inner_args |
|
instance_exec *args, &inner |
|
} |
|
end |
|
} |
|
} |
|
|
|
CurriedPrintsParameters = Curry.(PrintsParameters) |
|
|
|
pc CurriedPrintsParameters.(1) |
|
pc CurriedPrintsParameters.(1).(2) |
|
pc CurriedPrintsParameters.(1).(2).(3) |
|
pc CurriedPrintsParameters.(1).(2).(3).(4) |
|
pc CurriedPrintsParameters.(1).(2).(3).(4).(5) |
|
|
|
# Great! So now you have a basic working definition of curry. |
|
|
|
# But it turns out that as of Ruby 1.9 there is built-in way to do all of this (and more) |
|
# as the `Proc#curry` method. |
|
# |
|
# `Proc#curry` returns a proc. If you call the proc with the same arity |
|
# as the original Proc, it just calls through to the original (as above): |
|
|
|
pc PrintsParameters.curry.call(1, 2, 3, 4, 5) |
|
|
|
# If you call it with more than the original params, it blows up: |
|
|
|
begin |
|
PrintsParameters.curry.call(1, 2, 3, 4, 5, 6) |
|
rescue => e |
|
pc e.message |
|
end |
|
|
|
# If you call it with fewer parameters than the original, |
|
# it returns new another curry'd proc, just like NestsAllAndCallsLast. |
|
|
|
pc PrintsParameters.curry.call('Turtlz', 'Turtlz', 'Turtlz', 'Turtlz').call(5) |
|
pc PrintsParameters.curry.call('Turtlz', 'Turtlz').call('Turtlz', 'Turtlz').call(5) |
|
pc PrintsParameters.curry.call('Turtlz', 'Turtlz', 'Turtlz').call(4, 5) |
|
pc PrintsParameters.curry.call('Turtlz', 'Turtlz').call(3, 4, 5) |
|
pc PrintsParameters.curry.call('Turtlz').call(2, 3, 4, 5) |
|
|
|
# It handles all the recursion for you, and works on any Proc. |
|
# |
|
# As far as lambdas go, that's pretty much it for `Proc#curry`, but if you are |
|
# using Proc.new or Kernel#proc, there's one more thing that curry gives you. |
|
# |
|
# You can tell #curry how many parameters the new proc should |
|
# take, and it will pass nil to all the rest of the parameters. |
|
# Take this example: |
|
|
|
PrintsParametersProc = proc { |a, b, c, d, e, f| |
|
[a, b, c, d, e, f] |
|
} |
|
|
|
pc PrintsParametersProc.curry.call(2) |
|
pc PrintsParametersProc.curry.call(2).call(nil, nil, nil) |
|
pc PrintsParametersProc.curry(1).call(1) |
|
|
|
# Lambdas verify the arity that they are called with, but procs do not. |
|
# Since PrintsParameters takes 5 parameters, |
|
# unlike `Proc.new` or `Kernel#proc`, curry will raise an error unless |
|
# the given arity matches exactly, so it's not that useful: |
|
|
|
begin |
|
PrintsParameters.curry(1) |
|
rescue => e # => wrong number of arguments (1 for 5) |
|
pc e.message, -2 |
|
end |
|
|
|
begin |
|
PrintsParameters.curry(2) |
|
rescue => e # => wrong number of arguments (2 for 5) |
|
pc e.message, -2 |
|
end |
|
|
|
begin |
|
PrintsParameters.curry(3) |
|
rescue => e # => wrong number of arguments (3 for 5) |
|
pc e.message, -2 |
|
end |
|
|
|
begin |
|
PrintsParameters.curry(4) |
|
rescue => e # => wrong number of arguments (4 for 5) |
|
pc e.message, -2 |
|
end |
|
|
|
pc PrintsParameters.curry(5).call(1, 2, 3, 4, 5) |
|
|
|
Foo = ->(a, &block) { |
|
block.call |
|
} |
|
|
|
pc Foo.call('a') { 'this works' } |
|
|
|
begin |
|
Foo.curry.call('a').call { 'this does not work' } |
|
rescue => e |
|
pc e.message, -2 |
|
end |