The 'then' Ruby Keyword - What is it?

Have you heard of the then keyword?

This allows you to curry.

# this is a contrived example
# experiment with it in an IRB console.

# we want to:
# take a string
# add convert it to an int
# add 1 to that
# and then cube it
# returning the result

def operation_1(a)      
      a_int = (a.to_i)
      sum = a_int + 1
      cube = sum ** 3
      return cube
end

# is it pleasant to read?

puts "operation_1 is: #{operation_1("3")}"
# => operation_1 is: 64

# notice how the input of one function
# is an input into another?
# we can write the same thing like this:

def operation_2_dense(a)
      (a.to_i + 1) ** 3
end

puts "operation_2_dense is: #{operation_2_dense("3")}"
# => operation_1 is: 64

# but is it readable?

# you can also curry it like so:
def curry(a)
      a.then { |i| i.to_i }
       .then { |i| i + 1 }
       .then { |i| i ** 3 }      
end

puts "curry is #{curry("3")}"
# => curry is 64

def add_one(i)
      i + 1
end

def cube(i)
      i ** 3
end

def curry_2(a)
      a.then { |i| i.to_i }
       .then { |i| add_one(i) }
       .then { |i| cube(i) }      
end

puts "curry_2 is #{curry_2("3")}"
# => curry_2 is 64

# if you want something more readable.
# and if you like, you could put add_one and cube
# into a mixin. The implementation details are hidden
# and you can cleanly / elegantly express
# what you want.

Which do you prefer?

Extracted in the Wild:

I peaked into 37 Signal’s Writebook offering, from their Once suite of products.

### first_run.rb
User.create!(user_params.merge(role: :administrator))
     .tap do |user|
            DemoContent.create_manual(user)
end

They’re using tap. We can improve upon the above (IMO), by using then (which is more naturally expressive than ‘tap’) and by also creating a helper method:

User.create_admin(user_params).then do |user|
  DemoContent.create_manual(user)
end

Or take the pagy codebase:

def next_cursor
      hash = keyset_attributes_from(@records.last)
      json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
      B64.urlsafe_encode(json)
end

# or would you prefer it like this?

def next_cursor
      keyset_attributes_from(@records.last)
      .then { |attributes| @vars[:jsonify_keyset_attributes]&.(attributes) || attributes.to_json }
      .then { |json| B64.urlsafe_encode(json) }
end

It all depends on what style you prefer.

Ruby is an expressive language. You can say the same thing a million different ways.

If you like being expressive, then you can craft a script that is akin to art - a beautiful poem - for the human reader, but something which a computer can crunch, and to produce an output that someone else finds valuable.

Here is the full ruby file if you’re interested.

Written on December 9, 2024