FuncTional Programming

ON RAILS

Functional / OO

name = "Robert" 
big_name = String.upcase(name)

IO.puts(name)      # => "Robert"
IO.puts(big_name)  # => "ROBERT"
name = "Robert" 
big_name = name.upcase

puts(name)        # => "Robert"
puts(big_name)    # => "ROBERT"
date_format = "%m/%d"
date_format_with_year = date_format << "/%Y"

puts(date_format_with_year) # => "%m/%d/%Y"
puts(date_format)           # => "%m/%d/%Y"

# 😢
a = [1, 2, 3]
b = a 

b.push(4)
a.push(5)

a # => [1, 2, 3, 4, 5]
b # => [1, 2, 3, 4, 5]

OOP IS GREAT

  • Colocation of "material" and "tools"
  • Clear boundaries between entities 
  • Powerful mental model of noun  class
  • Built-in, easy state management

FP IS GREAT

  • Immutable values are hard to screw up 
  • Pure functions are hard to screw up

I Want IT all

  • From OOP: 
    • Code organization 
    • Colocation of "material" and "tools"
    • Straightforward state management
  • From FP:
    • Immutable values 
    • Pure Functions

VALUE OBject

# Given some event times, 
# Find the check-ins for those event times
class Event::AttendanceChart::ChartBar
  include HasCheckInCounts 

  def initialize(event_times)
    @event_times = event_times
  end

  def check_ins
    @check_ins ||= CheckIn
      .includes(:check_in_times)
      .where("check_in_times.event_time_id" => @event_times.pluck(:id))
  end
end

Public API is immutable data

Function OBject

class Location::FilterSentence::MaxMinFilterString
  def initialize(stringify_value:)
    @stringify_value = stringify_value
  end

  def comparison_string(min, max)
    if min.present? && max.present?
      if min == max
        value_string(min)
      else
        "#{value_string(min)} to #{value_string(max)}"
      end
    elsif min.present?
      "#{value_string(min)} and Up"
    elsif max.present?
      "Up to #{value_string(max)}"
    else
      nil
    end
  end

  private

  def value_string(val)
    @stringify_value.call(val)
  end
end

Public API is pure functions

Object OBject

class Event::AttendanceChart::ChartRange
  attr_reader :ends_at, :starts_at, :dates
  
  def initialize(ends_at:, range:, period:)
    @ends_at = ends_at
    @starts_at = send("starts_at_#{range}", @ends_at) 
    date_range = (starts_at.to_date..@ends_at.to_date)
    @dates = send("group_by_#{period}", date_range)
  end

  private

  def starts_at_this_month(ends_at)
    # ...
  end

  def starts_at_this_year(ends_at)
    # ...
  end

  def group_by_day(range)
    # ...
  end

  def group_by_week(range)
    # ...
  end
end

Public API is data, 

methods are pure functions

Sources

"Using a Ruby Class To Write Functional Code", Pat Shaunessy

 

"A Functional Pattern System for Object-Oriented Design", 

    Thomas Kuhne

 

"Commanding Objects Toward Immutability", Jim Gay

FP on Rails

By Robert Mosolgo

FP on Rails

Apply functional concepts to object-oriented code

  • 673