Implementing Currying in Ruby. A Step-by-Step Guide

4 min read

Currying is a functional programming technique that converts functions with multiple arguments into a sequence of functions with a single argument. When a function is curried, it takes one argument and returns a new function that expects the remaining arguments.

currying in ruby

Implementing currying in Ruby

Ruby provides built-in support for currying functions using the curry method. You can use this method on objects of the Proc class, or any other objects that implement the to_proc method.

When the curry method is called on a Proc object, it returns a new Proc object that represents a curried version of the original function. The curried function can be called with fewer arguments than it expects, and will return a new function expecting the remaining arguments.

A small example for understanding:

addition = proc { |a, b| a + b }.curry
result = addition.call(10, 5) # => 15
plus_ten = addition.call(10) # => #<Proc:0x00007fc270795178>
plus_ten.call(5) # => 15

When calling the curry method, you can explicitly specify arity for the created curried function. That is, the function will be called if a given number of arguments are passed to it. For example:

printer = proc { |a, b, c, d| "a=#{a}, b=#{b}, c=#{c}, d=#{d}" }.curry(3)
print = printer.call('list 1') # => #<Proc:0x00007f49f003cfc8>
print = print.call('list 2') # => #<Proc:0x00007f1867910ba0>
print = print.call('list 3') # => "a=list 1, b=list 2, c=list 3, d="

Creating specialized functions

Currying allows us to create specialized versions of functions with predefined arguments. This is useful when we need to use a function with fixed values in a certain context.

For example, suppose we have a function “greet” that takes two arguments: a greeting and a name. We can curry this function by setting the value of one of the arguments in advance, and get a new function expecting only the remaining argument.

greet = proc { |greeting, name| "#{greeting}, #{name}!" }.curry
hello = greet.call("Hello") # => #<Proc:0x00007fc270795178>
hi = greet.call("Hi") # => #<Proc:0x00007fc270795178>
hello.call("Alice") # => "Hello, Alice!"
hi.call("Bob") # => "Hi, Bob!"

In the example above, we curry the greet function by fixing the value of the greeting (“Hello” and “Hi”) and get new “hello” and “hi” functions. We can now call these new functions, passing only the names, and get specialized greetings.

This approach is useful when we have a common function pattern, but with different argument values in different contexts. We can create specialized versions of this function with predefined argument values and use them in the right places.

Simplifying function composition

Currying also simplifies function composition, i.e. the sequential application of arguments from one function to another. This allows us to create chains of curried functions and pass values from one function to another.

For example, suppose we have two functions: “add”, which adds two numbers, and “multiply”, which multiplies two numbers. We can curry these functions and apply the arguments sequentially, creating a new function that performs both actions.

add = proc { |a, b| a + b }.curry
multiply = proc { |a, b| a * b }.curry
add_and_multiply = add.call(5).then(&multiply.call(3))
result = add_and_multiply.call(2) # => 21

In this example, we first curry the add function by fixing the value of the first argument to 5. Then we apply the “then” methodto successively call the multiply function with a fixed value of the first argument equal to 3. The result is a new function add_and_multiply, which first adds 5 to its argument and then multiplies the resulting value by 3.

The composition of curried functions allows us to create more flexible and modular function blocks that can be easily combined and reused in different scenarios.

Simplifying complex business conditions

Let’s imagine that we have a complex business logic with many conditions and we want it all to be displayed in one place and be readable. To do this, we will need to use the “case…when” operator property. When a proc is passed to when, when calls the method === on this proc , passing the value in case as an argument:

is_high = ->(h) { h >= 10 }
is_high.call(10) # => true
# the same as
is_high.(10) # => true
# the same as
is_high === 10 # => true
case 5
when is_high then puts 'High'
else
  puts 'Not High!'
end

Let us now look at a more complex example. For instance, let’s assume that we are in the business of purchasing printers and we need a system that will help us make the right choice: to buy a printer or not, depending on the incoming parameters. 

First, let’s declare basic predicates to work with Boolean logic and curry them (we’ll explain why a bit later):

Basically, a predicate is a method/function that returns a boolean value (true or false)

AND operator

All = lambda do |predicates, attributes|  
  predicates.reduce(true) { |memo, predicate| memo && predicate.(**attributes) }  
end.curry 

All it does is take an array of passed predicates and check that they all return true.

 OR operator

Any = lambda do |predicates, attributes|  
  predicates.reduce(false) { |memo, predicate| memo || predicate.(**attributes) }  
end.curry

Same thing, but checking for at least one predicate that returned true.

NOT operator

Not = -> (predicate, attributes) { !predicate.(**attributes) }.curry

Inverts the result of executing the passed predicate.

Now that we have the basic operators, we can start declaring predicates for business logic:

WithWifiSupport = -> (printer:, **) { printer.wifi_support }  
WithoutWifiSupport = Not.(WithWifiSupport)  
Expensive = -> (printer:, tax: 0, **) { printer.price + tax > 500 }

These are ordinary lambdas that take some arguments and return a boolean value.

Now for the fun part – writing business rules using the above predicates:

def check_proposal(attributes)
  case attributes  
  when All.([Not.(Expensive), WithWifiSupport])  
    puts "Best offer"  
  when All.([Expensive, WithWifiSupport])  
    puts "Decent offer"  
  when All.([Expensive, WithoutWifiSupport])  
    puts "Worst offer"  
  when Any.([WithoutWifiSupport, Expensive])  
    puts "Bad offer"  
  end  
end

Due to the fact that we have curried the basic predicate lambdas, when we pass them the first argument – an array – we get a new lambda as a result. Accordingly, when when tries to check a condition, it passes the value from case to this new lambda, which, already having  all arguments, is executed and returns either true or false.

Let’s check if our code works:

Printer = Struct.new(:wifi_support, :price, keyword_init: true)
printer1 = Printer.new(wifi_support: true, price: 400)
printer2 = Printer.new(wifi_support: true, price: 800)
printer3 = Printer.new(wifi_support: false, price: 200)
check_proposal(printer: printer1, tax: 50) # Best offer
check_proposal(printer: printer2) # => Decent offer
check_proposal(printer: printer3) # Bad offer
check_proposal(printer: printer3, tax: 400) # Worst offer

The resulting code:

All = lambda do |predicates, attributes|  
  predicates.reduce(true) { |memo, predicate| memo && predicate.(**attributes) }  
end.curry  
Any = lambda do |predicates, attributes|  
  predicates.reduce(false) { |memo, predicate| memo || predicate.(**attributes) }  
end.curry  
Not = -> (predicate, attributes) { !predicate.(**attributes) }.curry  
WithWifiSupport = -> (printer:, **) { printer.wifi_support }  
WithoutWifiSupport = Not.(WithWifiSupport)  

Expensive = -> (printer:, tax: 0, **) { printer.price + tax > 500 }  
def check_proposal(attributes)
  case attributes  
  when All.([Not.(Expensive), WithWifiSupport])  
    puts "Best offer"  
  when All.([Expensive, WithWifiSupport])  
    puts "Decent offer"  
  when All.([Expensive, WithoutWifiSupport])  
    puts "Worst offer"  
  when Any.([WithoutWifiSupport, Expensive])  
    puts "Bad offer"  
  end  
end
Printer = Struct.new(:wifi_support, :price, keyword_init: true)

printer1 = Printer.new(wifi_support: true, price: 400)
printer2 = Printer.new(wifi_support: true, price: 800)
printer3 = Printer.new(wifi_support: false, price: 200)

check_proposal(printer: printer1, tax: 50) # Best offer
check_proposal(printer: printer2) # => Decent offer
check_proposal(printer: printer3) # Bad offer
check_proposal(printer: printer3, tax: 400) # Worst offer

Summary

In general, currying is a useful tool for writing functional code in Ruby that can increase its flexibility and expressiveness. Understanding the concept of currying and applying it correctly can create cleaner, more modular, and more efficient code.

But, its application can also make code more difficult to understand. Also, curried functions can be inconvenient when debugging, especially if there are too many of them or they are used incorrectly.

In most cases, simpler and more straightforward solutions may be preferable to currying. Always evaluate the advantages and disadvantages of using it within the context of the specific task and the overall architecture of your project.

Missed our previous article? Follow our blog and let’s stay in touch!

Editor's Choice

Post Image
5 min read

How JetRuby Academy Ensures Developers’ Excellence: 5 central stages of the growth process!

This review sheds light on our approach to selecting top-quality engineers for our Ruby Academy and outlines how we facilitate their ongoing training…

Post Image
10 min read

The Missing Puzzle Piece: Integrating Business Analysts into Your Tech Strategy

  Business analysis is the process of analyzing a company’s operations in detail to determine what is and is not working and how…

Post Image
6 min read

Now you Can Integrate Kafka into your Ruby on Rails project like a Pro!

Mastering Kafka Initially, one of our projects at JetRuby Agency aimed to improve communication between two critical services. The existing communication setup was designed such…

Get the best content once a month!

Once a month you will receive the most important information on implementing your ideas, evaluating opportunities, and choosing the best solutions! Subscribe

Contact us

By submitting request you agree to our Privacy Policy