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.

B46604999114579857b57c1fdae868f1?s=128

johnschoeman

April 30, 2019
Tweet

Transcript

  1. Sprinkles of Functional Programming johnschoeman

  2. Hello, I'm John I work at johnschoeman

  3. Github /johnschoeman Mastadon johnschoeman@technology.social Twitter @john_at_aol_dot_com_at_gmail_dot_com johnschoeman

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

    Some Code Recap / Action Item Questions johnschoeman
  5. Thesis johnschoeman

  6. Ruby is a general purpose language johnschoeman

  7. class A < B Object.new define_method(:foo) -> (x) { |x|

    x + 1 } data.map yield_self / then johnschoeman
  8. Different paradigms lend themselves to different tasks johnschoeman

  9. OO -> Behavior FP -> Data johnschoeman

  10. We should choose our programming style based off the task

    at hand johnschoeman
  11. If you are modeling behavior, prefer classes and composition. If

    you are handling data, prefer data pipelines and folds. johnschoeman
  12. FP and OO johnschoeman

  13. Objects and functions are a useful distinction. johnschoeman

  14. What is difficult to change is perhaps a better distinction.

    johnschoeman
  15. New Method New Data OO Existing code unchanged Existing code

    changed FP Existing code changed Existing code unchanged johnschoeman
  16. OO Typical Cases • Resource modeling • Behaviour modeling •

    State modeling johnschoeman
  17. FP Typical Cases • Data transformations • Data aggregates •

    Data streaming johnschoeman
  18. A Recommendation johnschoeman

  19. Given that methods are easy to add in OO and

    data is easy to add in FP johnschoeman
  20. Ask how you expect requirements will change before beginning a

    task johnschoeman
  21. Base this question off the context of your business and

    what users might want johnschoeman
  22. Choose your style based off of answer to this question

    johnschoeman
  23. A Story with some code johnschoeman

  24. Imagine a Rails App johnschoeman

  25. johnschoeman

  26. Initial Requirement Allow users to upload a csv johnschoeman

  27. Initial Requirement johnschoeman

  28. Commit 0 Allow users to upload csv of products johnschoeman

  29. 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
  30. class ImportsController < ApplicationController def create Product.import(params[:file].path) redirect_to products_path, notice:

    "Succesfully imported" end end johnschoeman
  31. 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
  32. 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
  33. - app - controllers imports_controller.rb - models - product.rb johnschoeman

  34. - spec - fixtures - products.csv - features - user_uploads_a_products_file_spec.rb

    - models - product_spec.rb johnschoeman
  35. Initial Requirement Gif johnschoeman

  36. johnschoeman

  37. New Requirement csv + xlsx johnschoeman

  38. Commit 1 Allow users to upload csv of products Introduce

    product data importer johnschoeman
  39. 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
  40. 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
  41. class ImportsController < ApplicationController def create importer = ProductDataImporter.new(params[:file].path) importer.import

    redirect_to products_path, notice: "Succesfully imported" end end johnschoeman
  42. Commit 2 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter johnschoeman
  43. 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
  44. 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
  45. Commit 3 Allow users to upload csv of products Introduce

    product data importer Introduce product data formatter Allow .xlsx format for importer johnschoeman
  46. 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
  47. 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
  48. New Requirement New currency format $ johnschoeman

  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. class FileImporter attr_reader :filepath, :formatter def initialize(filepath, formatter) @filepath =

    filepath @formatter = formatter end def import raise "Must overwrite method" end end johnschoeman
  57. class XlsxImporter < FileImporter ... end class CsvImporter < FileImporter

    ... end johnschoeman
  58. 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
  59. 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
  60. class ProductDataBuilder attr_reader :formatter def initialize(formatter) @formatter = formatter end

    def build raise "Must override method" end end johnschoeman
  61. 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
  62. class CsvBuilder < ProductDataBuilder def build(csv_row) data = csv_row.to_h.symbolize_keys formatter.format(data)

    end end johnschoeman
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. And a New Requirement... johnschoeman

  69. - 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
  70. - 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
  71. Functional Path johnschoeman

  72. New Requirement xlsx + csv johnschoeman

  73. Commit 1 Allow users to upload csv of products Introduce

    product importer service johnschoeman
  74. 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
  75. 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
  76. Commit 2 Allow users to upload csv of products Introduce

    product importer service Introduce data pipeline johnschoeman
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. New Requirement New currency format $ johnschoeman

  83. 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
  84. - spec - fixtures - products.csv - products.xlsx johnschoeman

  85. 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
  86. 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
  87. 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
  88. 1. Clearer Code johnschoeman

  89. - 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
  90. - 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
  91. - app - models - product.rb - services - product_data_importer.rb

    johnschoeman
  92. - spec - fixtures - products.csv - products.xlsx - services

    - product_data_importer_spec.rb johnschoeman
  93. 2. Less Code johnschoeman

  94. Total Diff johnschoeman

  95. Accumulated Diff johnschoeman

  96. Easier to Test / Maintain Style Public APIs added OO

    8 FP 1 johnschoeman
  97. Higher Development Velocity Style Dev Time OO ~ A day

    FP ~ An hour johnschoeman
  98. 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
  99. 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
  100. UML is a Linked List johnschoeman

  101. Take Aways johnschoeman

  102. It's useful to consider how requirements might change johnschoeman

  103. Sprinkles, just a little goes a long way johnschoeman

  104. Be conscientious of business goals before starting work. johnschoeman

  105. 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
  106. 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
  107. FP Learning Resources Gary Bernhardt - Functional Core, Imperative Shell

    thoughtbot.com/blog Piotr Solnica - Blending Functional and OO Programming in Ruby johnschoeman
  108. Repo johnschoeman/sprinkles-of-functional-programming-app johnschoeman

  109. Questions johnschoeman