Skip to content

Instantly share code, notes, and snippets.

@searls
Last active September 18, 2018 03:21
Show Gist options
  • Save searls/b68f8ce12089b053ec22b2dae35722f7 to your computer and use it in GitHub Desktop.
Save searls/b68f8ce12089b053ec22b2dae35722f7 to your computer and use it in GitHub Desktop.
Was chatting with @mfeathers about retaining Ruby's chained Enumerable style, but finding a way to inject names that reflects the application domain (as opposed to just littering functional operations everywhere, which may be seen as a sort of Primitive Obsession)
# A little toy file demonstrating how to build chainable
# data transformations that reveal some amount of intent
# through named extracted methods.
#
# Kudos to @mfeathers for giving me the idea to try this
#
# Copyright Test Double, LLC, 2016. All Rights Reserved.
require_relative "marketing_refinements"
class MarketResearch
# Vanilla / Anonymous / Primitive approach to chaining
def income_by_smoking(data)
Hash[
data.reject {|p| p[:income] < 10_000 }.
group_by { |p| p[:smoker] }.
map { |(is_smoker, people)|
[
is_smoker ? :smokers : :non_smokers,
people.map {|p| p[:income]}.reduce(:+).to_f / people.size
]
}
]
end
# Refined approach to tacking named domain abstractions onto Array/Hash
using MarketingRefinements
def income_by_smoking_fancy(data)
data.exclude_incomes_under(10_000).
separate_people_by(:smoker).
average_income_by_smoking
end
end
DATA = [
{age: 19, smoker: false, income: 10_000, education: :high_school},
{age: 49, smoker: true, income: 120_000, education: :bachelors},
{age: 55, smoker: false, income: 400_000, education: :masters},
{age: 23, smoker: true, income: 10_000, education: :bachelors},
{age: 70, smoker: false, income: 70_000, education: :phd },
{age: 34, smoker: false, income: 90_000, education: :masters},
{age: 90, smoker: true, income: 0, education: :high_school},
]
original_result = MarketResearch.new.income_by_smoking(DATA)
fancy_result = MarketResearch.new.income_by_smoking_fancy(DATA)
puts <<-MSG
Original result: #{original_result}
Fancy result: #{fancy_result}
MSG
module MarketingRefinements
refine Array do
# Domain-specific
def exclude_incomes_under(min)
reject {|p| p[:income] < min }
end
def separate_people_by(attribute)
group_by { |p| p[attribute] }
end
# General-purpose
def average(attr)
map {|el| el[attr]}.reduce(:+).to_f / size
end
end
refine Hash do
# Domain-specific
def average_income_by_smoking
Hash[
transform_keys { |is_smoker|
is_smoker ? :smokers : :non_smokers
}.map {|key, people|
[key, people.average(:income)]
}
]
end
# General-purpose
def transform_keys
{}.tap do |result|
self.each_key do |key|
result[yield(key)] = self[key]
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment