Skip to content

Instantly share code, notes, and snippets.

@eljojo
Created December 8, 2015 19:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eljojo/3682cdbde11f49d6a545 to your computer and use it in GitHub Desktop.
Save eljojo/3682cdbde11f49d6a545 to your computer and use it in GitHub Desktop.
some playing with functional programming and ruby
# A HashManipulation is a helper object.
# The idea is to aid when modifying hashes.
# changes = HashManipulation.new
# changes.key { |key| key.upcase }
# changes.value { |value| value.length }
# changes.("children" => [1, 2, 3]) # {"CHILDREN" => 3}
module Reports
class HashManipulation
def initialize
@key = -> (obj) { obj }
@value = -> (obj) { obj }
end
def key(&block)
@key = block
self
end
def value(&block)
@value = block
self
end
def call(obj)
obj.map do |key, value|
[@key.call(key), @value.call(value)]
end.to_h
end
def self.value(&block)
self.new.value(&block)
end
def self.key(&block)
self.new.key(&block)
end
end
end
# A Pipe is intended to be simple way of doing function composition (?)
module Reports
class Pipe
def initialize(procs = [])
@procs = procs
end
delegate :<<, to: :@procs
def call(obj)
@procs.inject(obj) { |memo, func| func.(memo) }
end
end
end
module Reports
class RepeatSwitches
def initialize
@contract_data = ContractDatum.all.ordered.includes(:user)
end
def report
group_by_first_or_second_switch = procerize(:group_by) do |contract|
first_switch?(contract) ? "First Switch" : "Repeat Switch"
end
get_counts = HashManipulation.value {|values| values.length }
get_switches_count = HashManipulation.value do |contracts|
get_counts.(group_by_first_or_second_switch.(contracts))
end
add_totals = HashManipulation.value do |hash|
hash.dup.tap do |res|
res["Total"] = hash.values.inject(:+)
end
end
pipe = Pipe.new
pipe << get_switches_count
pipe << add_totals
final_function = group_by_month(pipe)
final_function.(@contract_data)
end
def group_by_month(action)
group_by_month = procerize(:group_by) do |contract_datum|
ordered_at = contract_datum.ordered_at
[ordered_at.year, ordered_at.mon]
end
sort_by_first = -> (hash) { hash.sort_by(&:first).to_h }
humanize_month = HashManipulation.key{|(year, month)| "#{year}/#{month}" }
pipe = Pipe.new
pipe << group_by_month
pipe << action
pipe << sort_by_first
pipe << humanize_month
pipe
end
private
def first_switch?(contract_datum)
ordered_at = ContractDatum.arel_table[:ordered_at]
user_contracts = contract_datum.user.contract_data.ordered
user_contracts.where(ordered_at.lt(contract_datum.ordered_at)).empty?
end
def procerize(method, &block)
-> (obj) { obj.send(method, &block) }
end
end
end
@plexus
Copy link

plexus commented Dec 8, 2015

Good job! I tend to stick functional helper functions in a module that extends itself, so I can call them as "static" methods, or in places where I use them a lot I can include the module

module F
  extend self

  def map_values(&blk)
    ->(hsh) { Hash[hsh.map {|k,v| [k, blk.(v) }] }
  end

  def map_keys(&blk)
    ->(hsh) { Hash[hsh.map {|k,v| [blk.(k), v }] }
  end

  def procerize(method, *args, &block)
    -> (obj) { obj.send(method, *args, &block) }
  end

I've generalized your "procerize" a bit here, I've used something similar in some of my code but never could find a good name for it. It's basically "Symbol#to_proc, but also bind variables or a block". I've had it called send_with_args or (I kid you not) σ, but I admit neither of those really stuck :)

  include F

  def report
    group_by_first_or_second_switch = procerize(:group_by) do |contract|
      first_switch?(contract) ? "First Switch" : "Repeat Switch"
    end
    get_counts = map_values(&:length)
    get_switches_count = map_values do |contracts|
      get_counts.(group_by_first_or_second_switch.(contracts))
    end

One interesting observation is that both your HashManipulations and your Pipe are mutable. Why not do something like this?

class Pipe
  def initialize(procs)
    @procs = procs
  end

  def <<(proc)
    self.class.new(@procs + [proc])
  end

  def self.<<(proc)
    new([proc])
  end

  #...
end

That way you can do

    pipe = Pipe << group_by_month << action << sort_by_first << humanize_month

and you can reuse "partial" pipes

  by_month = Pipe << group_by_month
  monthly_revenue = by_month << sum_revenue
  monthly_orders = by_month << count_orders

In any case I think it's very cool you're trying these kind of things out. I'll start working on the Happy Lambda again at the end of the year (two more weeks left at my current job), so it's good to get some mental stimulation :D

@eljojo
Copy link
Author

eljojo commented Dec 8, 2015

@plexus hey this is super cool! Thanks a lot for your help!
I think I finally found a good example (for myself) so I can practice some functional programming.

thanks for the motivation as well :)

@bitboxer
Copy link

bitboxer commented Dec 8, 2015

@plexus was faster and way more insightfull than I would have been 👍 .

@eljojo
Copy link
Author

eljojo commented Dec 9, 2015

@bitboxer the intention still counts :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment