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

The Fun in Functional

Devon Estes
November 25, 2016

The Fun in Functional

Functional programming is all the rage these days, and many Rubyists feel left out of all the fun. But don't despair - many of the great ideas that are coming out of the functional revolution are applicable to our Ruby code, too! In this talk we'll explore some of the ways we can make our Ruby code more functional (pun intended) without giving up any of the joys of object orientation. In this talk you'll see how using some functional programming concepts can make your Ruby code easier to test, easier to maintain, and easier to change!

Devon Estes

November 25, 2016
Tweet

More Decks by Devon Estes

Other Decks in Programming

Transcript

  1. “First, make the change easy (warning: this may be hard),

    then make the easy change.” - Kent Beck
  2. “First, become Aaron Patterson or Justin Searls (warning: this may

    be hard), then write the presentation.” - Me
  3. &

  4. )

  5. Functional Programming to the Rescue! Immutable data structures Lazy or

    delayed evaluation Pure functions Higher order functions
  6. Functional Programming to the Rescue! Immutable data structures Lazy or

    delayed evaluation Pure functions Higher order functions Curried functions
  7. Functional Programming to the Rescue! Immutable data structures Lazy or

    delayed evaluation Pure functions Higher order functions Curried functions
  8. Ruby to the Rescue! Immutable data structures Lazy or delayed

    evaluation Pure functions Higher order functions
  9. Ruby to the Rescue! Immutable data structures Lazy or delayed

    evaluation Pure functions Higher order functions Curried functions
  10. I ❤ # => ruby client.rb data_for_team 'New York Jets'

    # # => ruby client.rb data_for_game 'New York Jets' 'Miami Dolphins' '11/06/2016' # client = NFLData::Client.new(api_key: ENV['NFL_API_KEY']) case ARGV[0] when 'data_for_team' puts client.data_for_team(team_name: ARGV[1]) when 'data_for_game' puts client.data_for_game(team1: ARGV[1], team2: ARGV[2], date: ARGV[3]) else 'Not a recognized command' end
  11. module NFLData class Client def initialize(api_key:) @api_key = api_key end

    def data_for_team(team_name:) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" raw = fetch_raw_data(url) add_turnover_data(raw) add_win_percentage(raw) add_how_team_is_doing(raw) end def data_for_game(team1:, team2:, date:) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" raw = fetch_raw_data(url) add_rushing_comparison(raw) add_passing_comparison(raw) end private # ...
  12. private attr_reader :api_key def fetch_raw_data(url) uri = URI.parse(url) json =

    Net::HTTP.get(uri) JSON.parse(json) end def add_turnover_data(raw) raw[:turnover_differential] = raw[:num_defensive_turnovers].to_i - raw[:num_offensive_turnovers].to_i end def add_win_percentage(raw) total_games = raw[:num_wins] + raw[:num_losses] raw[:win_percentage] = raw[:num_wins].to_f / total_games.to_f end def add_how_team_is_doing(raw) def add_rushing_comparison(raw) def add_passing_comparison(raw) end end
  13. module NFLData class Client def initialize(api_key:) @api_key = api_key end

    def data_for_team(team_name:) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" raw = fetch_raw_data(url) add_turnover_data(raw) add_win_percentage(raw) add_how_team_is_doing(raw) end def data_for_game(team1:, team2:, date:) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" raw = fetch_raw_data(url) add_rushing_comparison(raw) add_passing_comparison(raw) end private # ...
  14. module NFLData class Client def initialize(api_key:) @api_key = api_key end

    def data_for_team(team_name:) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" raw = fetch_raw_data(url) add_turnover_data(raw) add_win_percentage(raw) add_how_team_is_doing(raw) end def data_for_game(team1:, team2:, date:) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" raw = fetch_raw_data(url) add_rushing_comparison(raw) add_passing_comparison(raw) end private # ...
  15. team_client = NFLData::Team.new(api_key: ENV['NFL_API_KEY']) game_client = NFLData::Game.new(api_key: ENV['NFL_API_KEY']) case ARGV[0]

    when 'data_for_team' puts team_client.get_data(team_name: ARGV[1]) when 'data_for_game' puts game_client.get_data(team1: ARGV[1], team2: ARGV[2], date: ARGV[3]) else 'Not a recognized command' end
  16. module NFLData class Base def initialize(api_key:) @api_key = api_key end

    private attr_reader :api_key def fetch_raw_data(url) uri = URI.parse(url) json = Net::HTTP.get(uri) JSON.parse(json) end end end
  17. module NFLData class Game < NFLData::Base def get_data(team1:, team2:, date:)

    url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" @raw = fetch_raw_data(url) add_rushing_comparison add_passing_comparison add_defensive_comparison end private attr_reader :raw def add_rushing_comparison def add_passing_comparison def add_defensive_comparison end end
  18. module NFLData class Team < NFLData::Base def get_data(team_name:) url =

    "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" @raw = fetch_raw_data(url) add_turnover_data add_win_percentage add_how_team_is_doing end private attr_reader :raw def add_turnover_data def add_win_percentage def add_how_team_is_doing end end
  19. module NFLData class Game < NFLData::Base def get_data(team1:, team2:, date:)

    url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" @raw = fetch_raw_data(url) add_rushing_comparison add_passing_comparison add_defensive_comparison end private attr_reader :raw def add_rushing_comparison def add_passing_comparison def add_defensive_comparison end end
  20. module NFLData class Game < NFLData::Base def get_data(team1:, team2:, date:)

    url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" @raw = fetch_raw_data(url) add_rushing_comparison add_passing_comparison add_defensive_comparison end private attr_reader :raw def add_rushing_comparison def add_passing_comparison def add_defensive_comparison end end
  21. module NFLData class Base def initialize(api_key:, fetcher:) @api_key = api_key

    @fetcher = fetcher end private attr_reader :api_key, :fetcher end end
  22. module NFLData class Game < NFLData::Base def call(team1:, team2:, date:)

    url = "http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" @raw = fetcher.call(url) add_rushing_comparison add_passing_comparison add_defensive_comparison end private attr_reader :raw def add_rushing_comparison def add_passing_comparison def add_defensive_comparison end end
  23. irb(main):001:0> l = ->(str) { puts str.upcase } irb(main):002:0> l.call('hi')

    HI nil irb(main):003:0> str = 'hi' irb(main):004:0> p = Proc.new { puts str.upcase } irb(main):005:0> p.call HI nil irb(main):006:0> def up(str) irb(main):007:1> puts str.upcase irb(main):008:1> end :up irb(main):009:0> m = method(:up) Object#upp(str) irb(main):010:0> m.call('hi') HI nil irb(main):011:0> def call_block(&block) irb(main):012:1> block.call('hi') irb(main):013:1> end :call_block irb(main):014:0> call_block { |str| puts str.upcase } HI nil
  24. fetcher = -> (url) do uri = URI.parse(url) json =

    Net::HTTP.get(uri) JSON.parse(json) end team_client = NFLData::Team.new(api_key: ENV['NFL_API_KEY'], fetcher: fetcher) game_client = NFLData::Game.new(api_key: ENV['NFL_API_KEY'], fetcher: fetcher) case ARGV[0] when 'data_for_team' puts team_client.get_data(team_name: ARGV[1]) when 'data_for_game' puts game_client.get_data(team1: ARGV[1], team2: ARGV[2], date: ARGV[3]) else 'Not a recognized command' end
  25. client = NFLData::Client.new(api_key: ENV['NFL_API_KEY']) case ARGV[0] when 'data_for_team' puts client.data_for_team(team_name:

    ARGV[1]) when 'data_for_game' puts client.data_for_game(team1: ARGV[1], team2: ARGV[2], date: ARGV[3]) else 'Not a recognized command' end
  26. module NFLData class Client def initialize(api_key:) @api_key = api_key end

    def data_for_team(team_name:) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" raw = fetch_raw_data(url) add_turnover_data(raw) add_win_percentage(raw) add_how_team_is_doing(raw) end def data_for_game(team1:, team2:, date:) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" raw = fetch_raw_data(url) add_rushing_comparison(raw) add_passing_comparison(raw) end private # ...
  27. module NFLData class Client def initialize(api_key:, fetcher: default_fetcher) @api_key =

    api_key @fetcher = fetcher end def data_for_team(team_name:) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" NFLData::Team.new(fetcher: fetcher).call(url: url) end def data_for_game(team1:, team2:, date:) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" NFLData::Game.new(fetcher: fetcher).call(url: url) end private attr_reader :api_key, :fetcher def default_fetcher end end
  28. module NFLData class Game def initialize(fetcher:) @fetcher = fetcher end

    def call(url:) @data = fetcher.call(url) add_rushing_comparison add_passing_comparison add_defensive_comparison data end private attr_reader :fetcher, :data def add_rushing_comparison def add_passing_comparison def add_defensive_comparison end end
  29. module NFLData class Team def initialize(fetcher:) @fetcher = fetcher end

    def call(url:) @data = fetcher.call(url) add_turnover_data add_win_percentage add_how_team_is_doing data end private attr_reader :fetcher, :data def add_turnover_data def add_win_percentage def add_how_team_is_doing end end
  30. Pure Function Benefits Super easy to test Super easy to

    make thread safe Highly composable
  31. IO#read irb(main):001:0> IO.read('call.rb') "irb(main):001:0> l = ->(str) #..." irb(main):002:0> IO.read('call.rb')

    Errno::ENOENT: No such file or directory @ rb_sysopen - call.rb from (irb):2:in 'read' from (irb):2 from /Users/devoncestes/.rbenv/versions/2.3.1/bin/irb:11:in '<main>'
  32. String#downcase irb(main):001:0> str = 'I am a String' "I am

    a String" irb(main):002:0> str.downcase "i am a string"
  33. String#downcase irb(main):001:0> str = 'I am a String' "I am

    a String" irb(main):002:0> str.downcase "i am a string" irb(main):003:0> str "I am a String"
  34. String#<< irb(main):001:0> str = 'Is << pure?' "Is << pure?"

    irb(main):002:0> str << " Let's find out!" "Is << pure? Let's find out!"
  35. String#<< irb(main):001:0> str = 'Is << pure?' "Is << pure?"

    irb(main):002:0> str << " Let's find out!" "Is << pure? Let's find out!" irb(main):003:0> str "Is << pure? Let's find out!"
  36. client = NFLData::Client.new(api_key: ENV['NFL_API_KEY']) case ARGV[0] when 'team_data' puts client.team_data(team_name:

    ARGV[1]) when 'game_data' puts client.game_data(team1: ARGV[1], team2: ARGV[2], date: ARGV[3]) else 'Not a recognized command' end
  37. module NFLData class Client def initialize(api_key:, fetcher: default_fetcher) @api_key =

    api_key @fetcher = fetcher end def team_data(team_name:, metadata_adder: NFLData::Metadata::Team) url = "http://nfl.com/api/v1/teams/#{team_name}?api_key=#{api_key}" data = fetcher.call(url) metadata_adder.call(data: data) end def game_data(team1:, team2:, date:, metadata_adder: NFLData::Metadata::Game) url = “http://nfl.com/api/v1/games/#{date}?teams=#{team1},#{team2} &api_key=#{api_key}" data = fetcher.call(url) metadata_adder.call(data: data) end private attr_reader :api_key, :fetcher def default_fetcher end end
  38. module NFLData module Metadata class Team def self.call(**args) new(args).call end

    def initialize(data:) @data = data end def call add_turnover_data add_win_percentage add_how_team_is_doing data.freeze end private attr_reader :data # ... end end end
  39. module NFLData module Metadata class Game def self.call(**args) new(args).call end

    def initialize(data:) @data = data end def call add_rushing_comparison add_passing_comparison add_defensive_comparison data.freeze end private attr_reader :data # ... end end end
  40. Takeaways Method objects are your friend Avoid the mystery guest

    Look for pure functions ✨ Delay and group side effects
  41. Takeaways Method objects are your friend Avoid the mystery guest

    Look for pure functions ✨ Delay and group side effects Start thinking about the future