Lab42::Curry
Name says it all..
Curry functions and methods at will, reorder, define placeholders anywhere, positional and named args
N.B. All these code examples are verified with the speculate_about gem
So what does it do?
Context Positional Parameters
The simplest and classical way to curry a function (I include methods when I say function) is by providing the first
n
parameters to a function needing m
parameters and thusly defining a function that needs now m - n
parameters.
Given such a simple funcion
def adder(a, b, c); a + 10*b + 100*c end
let(:add_to_1) {curry(:adder, 1)}
# Equivalent to Elixir's &adder(1, &1, &2)
N.B. that Lab42::Curry
has been included into Examples and ExampleGroups in spec/spec_helper.rb
Then very unsurprisingly:
expect(add_to_1.(2, 3)).to eq(321)
We call the arguments passed into curry
the compiletime arguments, and the arguments passed into the
invocation of the curried function, which has been returned by the invocation of curry
, the runtime arguments.
In our case the compiletime arguments were [1]
and the runtime arguments were [2, 3]
Reordering
There are several methods of reordering arguments, the simplest is probably using placeholders.
When a placeholder is provided (Lab42::Curry.runtime_arg
aliased as rt_arg
)
let(:add_to_30) { curry(:adder, rt_arg, 3) }
# Equivalent to Elixir's &adder(&1, 3, &2)
Then we see that
expect( add_to_30.(1, 5) ).to eq(531)
Total control over argument order...
... can be achieved by passing the index of the positional argument to be used into Lab42::Curry.runtime_arg
Given the total reorder form
let(:twohundred_three) { curry(:adder, runtime_arg(1), 1, runtime_arg(0)) }
# now first argument is c (index 1) and second a (index 0) and b = 1
# Like Elixir's &adder(&2, 1, &1)
Then we have
expect( twohundred_three.(2, 3) ).to eq(213)
Picking a position for a compiletime argument
It might be cumbersome to write things like: curry(..., rt_arg, rt_arg, ..., rt_arg, 42)
Therefore we can express the same much more concisely with Lab42::Curry.compiletime_args
, and its alias ct_args
Given
let(:twohundred) { curry(:adder, ct_args(2 => 2)) }
# same as curry(:adder, rt_arg, rt_arg, 2)
Then we get
expect( twohundred.(4, 3) ).to eq(234)
N.B. that we could have defined add_to_30
as curry(:adder, rt_arg, 3, rt_arg)
of course
Error Handling
When you indicate values for the same position multiple times
Then the ArgumentCompiler
saves you:
expect{ curry(:adder, 1, ct_args(0 => 1)) }.to raise_error(Lab42::Curry::DuplicatePositionSpecification)
Context With proc like objects
Given a lambda
let(:sub) { ->{ _1 - _2} }
let(:inverse) { curry(sub, rt_arg(1), rt_arg) }
Then we will get the negative value
expect( inverse.(2, 1) ).to eq(-1)
Context Keyword Arguments
Given a function which takes keyword arguments like the following
def rectangle(length, width, border: 0, color: )
[length, width, border, color]
end
let(:red_rectangle) { curry(:rectangle, color: :red) }
let(:wide_bordered) { curry(:rectangle, rt_arg, 999, border: 1) }
Then the red rectangle gives us
expect( red_rectangle.(1, 2) ).to eq([1, 2, 0, :red])
expect( red_rectangle.(1, 2, border: 1) ).to eq([1, 2, 1, :red])
Can we override curried values, normally not Example: cannot override
expect{ red_rectangle.(1, 2, color: :blue) }
.to raise_error(
Lab42::Curry::DuplicateKeywordArgument,
"keyword argument :color is already defined with value :red cannot override with :blue")
But we can create a more lenient curry with curry!
expect( curry!(:rectangle, 1, 2, color: :red ).(color: :blue) )
.to eq([1, 2, 0, :blue])
Context Currying Blocks
Often times it is the block which might be the fixed point in a series of computations, for that reason we will curry a block
Given a function that takes a block
let(:sub_with) { ->(a, b, &blk) { blk.(a - b) } }
let(:double_diff) { curry( sub_with ) { _1 * 2 } }
Then it does just that
expect( double_diff.(22, 1) ).to eq(42)
And of course we can also curry a positional argument
triple_dec = curry( sub_with, rt_arg, 1 ) { _1 * 3 }
expect( triple_dec.(15) ).to eq(42)
Context Currying on Unbound Methods
Can be a very useful exercise, we will see that curry
creates a curred function that will bind when called
Given the classical map example:
let(:incrementer) { curry(Enumerable.instance_method(:map)) { _1 + 1} }
Then we can use it by binding it to an Enumerable
object
expect( incrementer.([1, 2]) ).to eq([2, 3])
But we must not provide a block again
expect{ incrementer.([]) {_1 + 2} }
.to raise_error(
Lab42::Curry::DuplicateBlock,
"block has already been curried")
This again can be authorized by using the more lenient curry!
version
And therefor
maybe_incrementer = curry!(Enumerable.instance_method(:map)) {_1 + 1}
expect( maybe_incrementer.([1, 2], &:itself) ).to eq([1, 2])
But of course we can also bind to unbound methods w/o a block
Given
let(:length) { curry(String.instance_method(:size)) }
Then we can call it in a functional way
expect( length.("") ).to be_zero
Context Computed Arguments
Let us say we want to define a specialisation of a function Given
def add a, b, c
a + b + c
end
let(:adddiff) { curry(:add, comp{ _2 - _3}) }
Then the first parameter will be the diff of the second and third argument
expect( adddiff.(21, 1) ).to eq(42)
When we specify a position for comp
the computed argument gets this place
def args *a
a
end
let(:sum_middle) { curry(:args, comp(1){ _1 + _3 } ) }
Then we get
expect( sum_middle.(1, 2) ).to eq([1, 3, 2])
Context Computed Keywords
Given
def kwds *, **k
k
end
let(:sum) { curry(:kwds, a: comp{ _1 + _2}) }
Then we can expect that sum corresponds
expect( sum.(1, 2) ).to eq({a: 3})
LICENSE
Copyright 2020,1 Robert Dober robert.dober@gmail.com
Apache-2.0 c.f LICENSE