Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sprinkles of Functional Programming

Sprinkles of Functional Programming

Often in Rails apps there is a need to accomplish tasks where we are less interested in modeling domain concepts as collections of related objects and more interested in transforming or aggregating data. Instead of forcing object oriented design into a role that it wasn’t intended for, we can lean into ruby’s functional capabilities to create Rails apps with sprinkles of functional code to accomplish these tasks.

johnschoeman

April 30, 2019
Tweet

Other Decks in Programming

Transcript

  1. Roadmap Thesis FP and OO A Recommendation A Story with

    Some Code Recap / Action Item Questions johnschoeman
  2. class A < B Object.new define_method(:foo) -> (x) { |x|

    x + 1 } data.map yield_self / then johnschoeman
  3. If you are modeling behavior, prefer classes and composition. If

    you are handling data, prefer data pipelines and folds. johnschoeman
  4. New Method New Data OO Existing code unchanged Existing code

    changed FP Existing code changed Existing code unchanged johnschoeman
  5. Given that methods are easy to add in OO and

    data is easy to add in FP johnschoeman
  6. Base this question off the context of your business and

    what users might want johnschoeman
  7. RSpec.describe "User uploads a file of produce data" do scenario

    "by visiting the root page and clicking 'upload file'" do visit root_path attach_file("file", Rails.root.join("spec/fixtures/products.csv")) click_on "Upload File" expect(Product.count).to eq 3 end end johnschoeman
  8. describe ".import" do context "the file is a csv" do

    it "saves every row in the file as new product" do filepath = stub_csv Product.import(filepath) expect(Product.count).to eq 3 end end end johnschoeman
  9. class Product < ApplicationRecord validates :name, presence: true ... def

    self.import(file_path) CSV.foreach(file_path, headers: true) do |row| data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.zone.parse(data[:release_date]) create(data) end end end johnschoeman
  10. RSpec.describe ProductDataImporter it "saves every row in the file as

    new product" do filepath = stub_csv importer = ProductDataImporter.new(filepath) importer.import expect(Product.count).to eq 3 end end johnschoeman
  11. class ProductDataImporter attr_reader :filepath def initialize(filepath) @filepath = filepath end

    def import CSV.foreach(filepath, headers: true) do |row| data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.zone.parse(data[:release_date]) Product.create(data) end end end johnschoeman
  12. Commit 2 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter johnschoeman
  13. Rspec.describe ProductDataFormatter it "builds a data hash from a csv_row

    for the product" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] csv_row = CSV::Row.new(headers, fields) formatter = ProductDataFormatter.new result = formatter.build(csv_row) expect(result).to eq(expected_results) end end johnschoeman
  14. class ProductDataFormatter def build(csv_row) data = csv_row.to_h.symbolize_keys data[:title] = data[:title].titleize

    data[:release_date] = Time.zone.parse(data[:release_date]) data end end johnschoeman
  15. Commit 3 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer johnschoeman
  16. Rspec.describe ProductDataImporter context "the file is a xlsx" do it

    "saves every row in the file as new product" do filename = "products.xlsx" stub_xlsx(filename) formatter = ProductDataFormatter.new importer = ProductDataImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
  17. class ProductDataImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath =

    filepath @formatter = formatter end def import case File.extname(filepath) when ".csv" import_csv when ".xlsx" import_xlsx end end private def import_csv CSV.foreach(filepath, headers: true) do |row| formatted_data = formatter.build(row) formatted_data = formatter.build_csv(row) Product.create(formatted_data) end end def import_xlsx Xlsx.foreach(filepath) do |row| formatted_data = formatter.build_xlsx(row) Product.create(formatted_data) end end ... end johnschoeman
  18. Commit 4 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer johnschoeman
  19. RSpec.describe CsvImporter describe "#import" do it "saves every row in

    the file as new product" do filename = "products.csv" stub_csv(filename) formatter = ProductDataFormatter.new importer = CsvImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
  20. class CsvImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath =

    filepath @formatter = formatter end def import CSV.foreach(filepath, headers: true) do |row| formatted_data = formatter.build_csv(row) Product.create(formatted_data) end end end johnschoeman
  21. RSpec.describe XlsxImporter describe "#import" do it "saves every row in

    the file as new product" do filename = "products.xlsx" stub_xlsx(filename) formatter = ProductDataFormatter.new importer = XlsxImporter.new(filename, formatter) importer.import expect(Product.count).to eq 3 end end end johnschoeman
  22. class XlsxImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath =

    filepath @formatter = formatter end def import Xlsx.foreach(filepath) do |row| formatted_data = formatter.build_xlsx(row) Product.create(formatted_data) end end end johnschoeman
  23. Commit 5 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer johnschoeman
  24. RSpec.describe FileImporter describe "#import" do it "raise if called" do

    importer = FileImporter.new("filepath", double("formatter")) expect { importer.import }.to raise_error("Must overwrite method") end end end johnschoeman
  25. class FileImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath =

    filepath @formatter = formatter end def import raise "Must overwrite method" end end johnschoeman
  26. Commit 6 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer Introduce data builder class johnschoeman
  27. RSpec.describe ProductDataBuilder describe "#build" do it "raises" do formatter =

    double builder = ProductDataBuilder.new(formatter) expect { builder.build }.to raise_error("Must override method") end end end johnschoeman
  28. RSpec.describe CsvBuilder do describe "#build" do context "when given a

    csv_row of product data" do it "creates a hash of the product data" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] csv_row = CSV::Row.new(headers, fields) formatter = ProductDataFormatter.new builder = CsvBuilder.new(formatter) result = builder.build(csv_row) expect(result).to eq({ ...data }) end end end end johnschoeman
  29. RSpec.describe XlsxBuilder do describe "#build" do context "when given a

    xlsx_row of product data" do it "creates a hash of the product data" do headers = %w[name author version release_date value active] fields = %w[name_a author_a 1.10.3 20190101 100 true] xslx_row = stub_xslx_row(headers, fields) formatter = ProductDataFormatter.new builder = CsvBuilder.new(formatter) result = builder.build(xlsx_row) expect(result).to eq({ ...data }) end end end end johnschoeman
  30. class XlsxBuilder < ProductDataBuilder HEADERS = %w[name author release_date value

    version active].freeze def build(xlsx_row) cells = xlsx_row.cells.map(&:value) data = HEADERS.zip(cells).to_h.symbolize_keys formatter.format(data) end end johnschoeman
  31. Commit 7 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer Refactor importer Introduce file importer Introduce data builder class Format currency data from csv johnschoeman
  32. RSpec.describe ProductDataFormatter do ... context "when the currency has a

    dollar sign" do it "strips the dollar sign" do data = { value: "$1230" } formatter = ProductDataFormatter.new result = formatter.format(data) expect(result).to eq({ value: 1230 }) end end johnschoeman
  33. class ProductDataFormatter def format(data) data[:title] = format_title(data[:title]) data[:release_date] = format_date(data[:release_date])

    data[:value] = format_currency(data[:value]) data end ... def format_currency(input) input.to_s.gsub(/^\$/, "").to_i end end johnschoeman
  34. - app - models - application_record.rb - csv_builder.rb - csv_importer.rb

    - file_importer.rb - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
  35. - spec - models - csv_builder_spec.rb - csv_importer_spec.rb - file_importer_spec.rb

    - product_spec.rb - product_data_builder_spec.rb - product_data_formatter_spec.rb - product_data_importer_spec.rb - xlsx_builder_spec.rb - xlsx_importer_spec.rb johnschoeman
  36. Commit 1 Allow users to upload csv of products Introduce

    product importer service johnschoeman
  37. RSpec.describe ProductDataImporter do describe ".import" do it "takes a file

    and creates product data from the data" do filename = Rails.root.join("spec/fixtures/products.csv") importer = ProductDataImporter.new expected_data = { ...data } importer.import(filename) expect(Product.count).to eq 3 expect_product_to_match(Product.last, expected_data) end end johnschoeman
  38. class ProductDataImporter def self.import(filepath) new.import(filepath) end def import(filepath) CSV.foreach(filepath, headers:

    true) do |row| data = row.to_h.symbolize_keys data[:title] = data[:title].titleize data[:release_date] = Time.parse(data[:release_date] Product.create(data) end end end johnschoeman
  39. Commit 2 Allow users to upload csv of products Introduce

    product importer service Introduce data pipeline johnschoeman
  40. class ProductDataImporter def self.import(filepath) new.import(filepath) end def import(filepath) read_file(filepath). map

    { |data| process_data(data) }. map { |data| Product.create(data) } end private def read_file(filepath) CSV.foreach(filepath, headers: true).map do |row| row.to_h.symbolize_keys end end def process_data(data) process_date(data). then { |data| process_title(data) } end def process_date(data) new_data = data.dup new_data[:release_date] = Time.zone.parse(data[:release_date]) new_data end def process_title(data) new_data = data.dup new_data[:title] = title.titleize new_data end end johnschoeman
  41. class ProductDataImporter ... def import(filepath) read_file(filepath). map { |data| process_data(data)

    }. map { |data| Product.create(data) } end private ... def process_data(data) process_date(data). then { |data| process_title(data) } end ... end johnschoeman
  42. Commit 3 Allow users to upload csv of products Introduce

    product importer service Introduce data pipeline Allow for users to upload either xlsx or csv johnschoeman
  43. RSpec.describe ProductDataImporter do ... context "when provided a xlsx file"

    do it "creates product data from the data" do filename = Rails.root.join("spec/fixtures/products.xlsx") importer = ProductDataImporter.new expected_data = { ...data } importer.import(filename) expect(Product.count).to eq 4 expect_product_to_match(Product.last, expected_data) end johnschoeman
  44. class ProductDataImporter ... def import(filepath) read_file(filepath). map { |data| process_data(data)

    }. map { |data| Product.create(data) } end private def read_file(filepath) case File.extname(filepath) when ".csv" read_csv(filepath) when ".xlsx" read_xlsx(filepath) end end def read_xlsx(filepath) foreach_xlsx(filepath).map do |row| cells = row.cells.map(&:value) data = HEADERS.zip(cells).to_h.symbolize_keys end end ... end johnschoeman
  45. Commit 4 Allow users to upload csv of products Introduce

    product importer service Introduce data pipeline Allow for users to upload either xlsx or csv Format currency data from csv johnschoeman
  46. class ProductDataImporter ... def process_data(data) process_date(data). then { |data| process_description(data)

    }. then { |data| process_currency(data) } end ... def process_currency(data) new_data = data.dup new_data[:value] = value.gsub(/^\$/, "").to_i new_data end end johnschoeman
  47. Recap • Initial Requirement: Clients Import CSV • and then

    New Requirement: .xlsx + .csv • and then New Requirement: New currency format • and then a New Requirement... johnschoeman
  48. Benefits of choosing the right paradigm for the task 1.Clearer

    Code 2.Less Code 3.Easier to Test and Maintain 4.Higher Development Velocity johnschoeman
  49. - app - models - csv_builder.rb - csv_importer.rb - file_importer.rb

    - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
  50. - spec - models - csv_builder_spec.rb - csv_importer_spec.rb - file_importer_spec.rb

    - product_spec.rb - product_data_builder_spec.rb - product_data_formatter_spec.rb - product_data_importer_spec.rb - xlsx_builder_spec.rb - xlsx_importer_spec.rb johnschoeman
  51. - spec - fixtures - products.csv - products.xlsx - services

    - product_data_importer_spec.rb johnschoeman
  52. Paradigm Smells Using OO for a task that lends itself

    to FP 1.Lots of 'Something-er' Classes 2.UML is a Linked List johnschoeman
  53. Lots of 'Something-er' Classes - app - models - csv_builder.rb

    - csv_importer.rb - file_importer.rb - product.rb - product_data_builder.rb - product_data_formatter.rb - product_data_importer.rb - xlsx_builder.rb - xlsx_importer.rb johnschoeman
  54. Action Item Before beginning a task, Ask how you expect

    requirements will change: Data or Behaviour? If Data, consider a functional style If Behavior, consider a object oriented style johnschoeman
  55. Thesis Ruby is a multi-paradigm language. Different paradigms lend themselves

    to different tasks We should choose our style based off of our task johnschoeman
  56. FP Learning Resources Gary Bernhardt - Functional Core, Imperative Shell

    thoughtbot.com/blog Piotr Solnica - Blending Functional and OO Programming in Ruby johnschoeman