Functional Data Flows (.select.map) to Create Dependent Records in Ruby/Rails

Mike Schutte
4 min readJun 21, 2018

--

You may have seen code in the past that looks something like this:

user = User.build(user_params)
if user.save
profile = Profile.build(user: user)
if profile.save
Yearbook.build(profile: profile, anniversary: Date.now)
end
end

Lets say a Yearbook is an object that keeps track of anniversaries and/or milestones. A user has one profile and can have many yearbooks. No need to worry about the particulars of the models or why the sequence is the way it is. Just take it at a high level as a situation where record creation and updates are dependent on other record creations and updates.

We have a sequence of record creations and updates, where:

  1. the second record ( Profile ) is dependent on the first ( User)
  2. the third record ( Yearbook) is dependent on the second ( Profile )

We assign the variables because it’s a fairly nice thing to do for readability and common in the Ruby and Rails world. Do the variables ever actually…vary?

Instead of using explicit, nestedif statements and assigning multiple variables to memory (that never actually change), let’s construct a functional data flow that leverages (1) a declarative style (with if statements operating in a private interface) and (2) automatically cleaned up resources with blocks (i.e., anonymous functions) and closures.

[User.build(user_params)].select(&:save)
.map { |user| Profile.build(user: user) }
.select(&:save)
.map { |profile| Yearbook.build(profile: profile,
anniversary: Date.now) }
.select(&:save)
.first

On the first line, we wrap a built User record in a singleton array (i.e., an array of one element). “Lifting” a single value into an array is like putting a value into a box, a box that has extra functionality. By putting a value (a record) into a context (an array), we get to use Ruby’s lovely Enumerable module to handle control flow and data transformations in a more declarative way.

On the singleton array, we call Enumerable#select using &:save as our predicate function for select (we are in ActiveRecord land, where .save returns true if the record is persisted to the database and false if not). If this is strange syntax to you:

[1,2].select(&:even?) == [1,2].select { |number| number.even? }

This is sugar for a common pattern of calling a single method with no arguments on each element in an array.

The select method will only return objects that have a “truthy” return value (i.e., not nil or false ) from the predicate function. In this case, if .save returns false , the returned array will be empty []. All subsequent .map and .select calls are no-ops. If the .save is successful and returns true , the returned array contains the saved User record. On to the second line!

Assuming .map is called on an array with a successfully saved User record, that record is passed to the .map block and used to build a Profile record. The returned array now contains a built Profile record instead of a saved User record.

On the third line, we use the same .save predicate sugar. Again, if the save is successful, the array retains its Profile record. If the save is not successful, the returned array is empty, and all subsequent calls are no-ops.

On the fourth line, we use the Profile record to build a Yearbook record.

On the fifth line, we filter for a successful save to the database.

Lastly, on the sixth line, we return the array’s first (and only) element. There are two return cases:

  1. true : All of the record creations were successful.
  2. nil : The creation of User , Profile , and/or Yearbook was unsuccessful. Because [].first == nil , at some point in the flow .save failed and returned false , leaving us [] for the rest of the flow.

Stepping Through Scenarios

*these snippets will not execute in Ruby because the comments break the method chain

  1. All successful creations
[User.build(user_params)].select(&:save)
# [<User id: 1 ...>]
.map { |user| Profile.build(user: user) }
# [<Profile id: nil ...>]
.select(&:save)
# [<Profile id: 1 ...>]
.map { |profile| Yearbook.build(profile: profile,
anniversary: Date.now) }
# [<Yearbook id: nil ...>]
.select(&:save)
# [<Yearbook id: 1...>]
.first # => <Yearbook id: 1 ...>

2. Failed Yearbook creation

[User.build(user_params)].select(&:save)
# [<User id: 1 ...>]
.map { |user| Profile.build(user: user) }
# [<Profile id: nil ...>]
.select(&:save)
# [<Profile id: 1 ...>]
.map { |profile| Yearbook.build(profile: profile,
anniversary: Date.now) }
# [<Yearbook id: nil ...>]
.select(&:save)
# []
.first # => nil

3. Failed Profile creation

[User.build(user_params)].select(&:save)
# [<User id: 1 ...>]
.map { |user| Profile.build(user: user) }
# [<Profile id: nil ...>]
.select(&:save)
# []
.map { |profile| Yearbook.build(profile: profile,
anniversary: Date.now) }
# []
.select(&:save)
# []
.first # => nil

4. Failed User creation

[User.build(user_params)]
# [<User id: nil ...>]
.select(&:save)
# []
.map { |user| Profile.build(user: user) }
# []
.select(&:save)
# []
.map { |profile| Yearbook.build(profile: profile,
anniversary: Date.now) }
# []
.select(&:save)
# []
.first # => nil

With each .select(&:save) we are producing a side-effect, so this flow is not 100% functionally pure, but it does turn the problem into a simple series of inputs and outputs. Additionally, the side-effects are nicely separated from transformations. You can see that in this flow, map takes care of transformations, while select takes care of mutations. This workflow allows us to isolate our focus on one type of problem at a time and think with more clarity and confidence.

By wrapping an object in an array, we were able to make a single, self-contained flow using Enumerable#map and Enumerable#select . This removed explicit uses of if statements (that’s being used by #select ) and moved variable assignment from an outer to a more private scope. It also illuminated a nice structural pattern that was harder to see with nested if statements. Overall, we used native Ruby features to write a lean, declarative, functional solution that subscribes more to an interface than an implementation

--

--

Mike Schutte

Licensed Driver 🚗 TSA Pre ® AeroPress Barista ☕️ Conversational in Emoji 🤟 Anti-mouse (💻✚🏠) Pro-listening 👂he/him 👨‍💻 @TEDTalks