Skip to content

RobertDober/lab42_curry

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gem Version CI Coverage Status

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

About

Name says it all :)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages