The 'then' Ruby Keyword - What is it?

Have you heard of the then method? I’d never heard of it, till now:

then 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:

The code works, but it ain’t very readable. Let’s try another approach:

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?

IMO it’s terser, but there’s still some mental overhead required to read and understand it. Let’s try currying:

# 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

That works! Let’s improve upon it:


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:

The best way of learning is by implementing it in some code base you are reading - preferably your own (at least that’s how I learn and experiment). Otherwise what tends to happen: you’ll read, and then completely forget about it. This is necessarily a problem looking for a solution, but that’s the only way you can advance by adding another tool in your ruby repertoire. At the very least, consider undertaking a similar exercise yourself:

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. Can improve upon the above, by using then? What would you do?

# my answer
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.

yield_self

then seems identical to the yield_self method.

Source Code for Experimentation

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

Updates

I fixed some errors, added some details.

Written on December 9, 2024