/ TIL

Breaking your mind currying functions in Ruby

I recently stumbled across an amazing, and mind breaking method in ruby. That method is curry. Here is the documentation for curry from ruby 2.4's docs.

https://ruby-doc.org/core-2.4.0/Proc.html#method-i-curry

This should look familiar to some people with knowledge of front-end javascript frameworks. Ember, for example, emphasized function currying in order to maintain a nice DDAU (Data Down Actions Up) pattern for your components. This technique also shows up in a lot of functional languages. Personally I never thought to use this sort of thing in Ruby, and I was really surprised to find that it was a part of the proc API for some time now.

Let's go over a simple example of currying in javscript.

  1. You have some curry-able function:
  let curriedFunction = function(arg) {
    return function(arg2) {
      return `${arg} is better than ${arg2}!`
    }
  }
  1. You call this function from somewhere in your code:
  let calledFunc = curriedFunction("peanut butter")
  1. You add another arg to this function somewhere else in your code:
  calledFunc("nutella")
  // 'peanut butter is better than nutella!'

This is an extremely powerful concept in javascript and other functional languages. With currying, you can pass around a pre-loaded function, do some computation, and call that function with another arg that you've created.

Now, Ruby doesn't have functions as first-class citizens like javascript. However, Ruby has something pretty close: Proc. A Proc is kind of like a function in javascript or elixir. You define it, bind it to a set of variables, and call it somewhere else.

Lets go over a simple example of currying a Proc in Ruby.

  1. You have some curry-able function in ruby (a Proc):
  def curryable_function
    ->(arg1, arg2) { "#{arg1} is better than #{arg2}!" }
  end
  1. You curry your Proc, and pass an initial value:
  curried = curryable_function.curry(2)["peanut butter"]
  1. You call this proc, passing in another arg you computed somewhere else:
  curried["nutella"]
  # peanut butter is better than nutella!

Now, Ruby has some syntastic oddities in it's curry implementation. Let's go over them.

  • when you have a Proc, you can call curry(<arity>) to curry it. The argument you pass in is the arity, or the number of args you expect total
  • you pass in an arg with square brackets ["peanut butter"]
  • when the proc reaches arguments you defined with the arity you passed to curry(), it gets called. in our example above, we use 2. Meaning our Proc gets called once two args have been passed in.

I think this is a very powerful tool in Ruby. Lets go over a real-life example to see it in action.

Lets say you have a class that generates an object with some information, a Game. Depending on where this Game is created, it needs to do some extra stuff after creation. And this 'extra stuff' is handled by a setup class GameSetup.

  class GameGenerator
    def initialize(after_create:)
      @after_create = after_create
    end
    
    def generate
      game = Game.create!
      after_create[game]
    end
  end

Then, in another class, you can generate different games:

  def generate_normal_game
    GameGenerator.new(after_create: setup.curry(2)["normal"]).generate
  end
  
  def generate_expert_game
    GameGenerator.new(after_create: expert_setup.curry(3)["expert", previous_game]).generate
  end

  def setup
    ->(type, game) { GameSetup(type, game) }
  end
  
  def expert_setup
    ->(type, previous_game, new_game) { ExpertGameSetup(type, previous_game, new_game) }
  end

I think this is an extremely interesting pattern. Instead of having a Generator class explicitly asking about what it's generating, you can pass in a callback pre-loaded with that information. Then your Generator class doesn't ask about itself, it just calls what was passed into it.

This also makes your top level API in the GameGenerator class that much easier to read. You generate a game, then call whatever Proc was passed in, giving it the game as an argument.

I am not sure if I'll be using this pattern in the near future with Ruby. But I think it has some potential when used correctly.

chris power

chris power

Chris is a full stack engineer working on multiple JS, Ruby, and Elixir projects. He is also an avid snowboarder, golfer, and beer snob.

Read More