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

FRiPpery

 FRiPpery

Talk on functional reactive programming, given at Ruby Manor 4.0.

If you weren’t there, I’m afraid this deck won’t be much use, as some of the slides had screen recordings of the terminal—you’ll have to wait for the video to go up to see those.

Aanand Prasad

April 06, 2013
Tweet

Other Decks in Programming

Transcript

  1. Input: Text on STDIN, HTTP request, clicking a button. Output:

    Text on STDOUT, HTTP response, showing an alert. Sunday, 7 April 13
  2. Imperative Ruby • Consuming input IO#read, event handlers, controller actions

    • Program state: mutable variables, ivars, mutable arrays/hashes • Producing output IO#write, API methods, render() Sunday, 7 April 13
  3. FRP • Consuming input: streams • Program state: streams •

    Producing output: streams Sunday, 7 April 13
  4. Streams are a bit like arrays. • map (a.k.a. collect)

    - do something with all the elements • select - give me some of the elements Sunday, 7 April 13
  5. 1 2 3 4 5 s 2 4 6 8

    10 s.map  {|x|  x*2  } Time Sunday, 7 April 13
  6. s s.scan(0)  {|sum,x|    sum  +  x } Time 1

    2 3 4 0 1 3 6 10 Sunday, 7 April 13
  7. 1 2 3 s a b t s.merge(t) 1 a

    2 3 b Time Sunday, 7 April 13
  8. total  =  0   while  line  =  gets    if

     line.strip.empty?        puts  "Total:  #{total}\n\n"        total  =  0    else        total  +=  line.to_f    end end Imperative Sunday, 7 April 13
  9. require  'wick/io'                

                                                                                                                  Wick::IO.bind(read:  $stdin,  write:  $stdout)  do  |input|                                                    initial  =  {total:  0}        state  =  input.scan(initial)  {  |s,  line|                                                                                  if  line.strip.empty?            {total:  0,  message:  "Total:  #{s[:total]}"}        else            {total:  s[:total]  +  line.to_f}        end    }        output  =  state.select  {  |s|  s.has_key?(:message)  }                                .map        {  |s|  s[:message]  +  "\n\n"  }      output end Reactive Sunday, 7 April 13
  10. input total:  0 state input.scan(initial)  {    ... } output

    state.filter  {...}          .map        {...} Sunday, 7 April 13
  11. require  'socket'   socket  =  TCPSocket.new(ARGV[0],  ARGV[1]  ||  23)  

    begin    while  true        ready  =  IO.select([$stdin,  socket])        ready[0].each  do  |io|            data  =  io.read_nonblock(1_000_000)  #YOLO              if  io.equal?($stdin)                socket.write(data)            elsif  io.equal?(socket)                $stdout.write(data)            end        end    end rescue  EOFError    puts  "A  stream  was  closed.  Shutting  down." end Imperative Sunday, 7 April 13
  12. require  'wick/io' require  'socket'     socket  =  TCPSocket.new(ARGV[0],  ARGV[1]

     ||  23)                                                                         Wick::IO.bind(      read:    [socket,  $stdin],    write:  [socket,  $stdout]   )  do  |network_in,  user_in|    network_out,  user_out  =  user_in,  network_in    [network_out,  user_out]   end Reactive Sunday, 7 April 13
  13. Wick::IO.bind(      [socket,  $stdin],    [socket,  $stdout]   )

     do  |network_in,  user_in|      #  ...create  network_out  and  user_out...      [network_out,  user_out]   end Sunday, 7 April 13
  14. server_events  =  network_in.map  {  |line|  IRCEvent.parse(line)  }      

          client                    =  Client.new(nick) client_commands  =  client.transform(server_events) network_out          =  user_in.merge(client_commands) Client Sunday, 7 April 13
  15. class  Client  <  Struct.new(:nick)    def  transform(server_events)      

     nick_and_user  =  Wick.from_array([            "NICK  #{nick}",            "USER  #{nick}  ()  *  #{nick}"        ])                    ping  =  server_events.select  {  |msg|  msg.command  ==  "PING"  }                                      pong  =  ping.map  {  |msg|  "PONG  "  +  msg.params.join("  ")  }                                                    nick_and_user.merge(pong)    end       end Client Sunday, 7 April 13
  16. ui              =  UI.new user_out

     =  ui.transform(client_commands,  server_events) UI Sunday, 7 April 13
  17. class  UI    def  transform(client_commands,  server_events)        incoming_messages

     =  server_events.select  {  |event|            event.command  ==  "PRIVMSG"        }        other_events  =  server_events.select  {  |event|            event.command  !=  "PRIVMSG"        }          message_lines  =  incoming_messages.map  {  |event|            channel  =  event.params[0]            user        =  "<#{event.user}>"            message  =  event.params[1]              "#{channel.green}  #{user.yellow}  #{message}"        }          message_lines            .merge(other_events.map  {  |event|  "<  #{event.line}".magenta  })            .merge(client_commands.map  {  |line|  ">  #{line}".magenta  })    end end UI Sunday, 7 April 13
  18. class  Client    def  transform(network_in,  user_commands)        server_events

     =  network_in.map  {  |line|  IRCEvent.parse(line)  }          nick_and_user  =  Wick.from_array([            "NICK  #{nick}",            "USER  #{nick}  ()  *  #{nick}"        ])          ping  =  server_events.select  {  |msg|  msg.command  ==  "PING"  }        pong  =  ping.map  {  |msg|  "PONG  "  +  msg.params.join("  ")  }          outgoing  =  process_user_commands(user_commands)          network_out  =  outgoing.merge(nick_and_user).merge(pong)          [network_out,  server_events]    end end Sunday, 7 April 13
  19. class  Client    def  transform(network_in,  user_commands)        server_events

     =  network_in.map  {  |line|  IRCEvent.parse(line)  }          nick_and_user  =  Wick.from_array([            "NICK  #{nick}",            "USER  #{nick}  ()  *  #{nick}"        ])          ping  =  server_events.select  {  |msg|  msg.command  ==  "PING"  }        pong  =  ping.map  {  |msg|  "PONG  "  +  msg.params.join("  ")  }          outgoing  =  process_user_commands(user_commands)          network_out  =  outgoing.merge(nick_and_user).merge(pong)          [network_out,  server_events]    end end Sunday, 7 April 13
  20. class  Client    def  transform(network_in,  user_commands)        server_events

     =  network_in.map  {  |line|  IRCEvent.parse(line)  }          nick_and_user  =  Wick.from_array([            "NICK  #{nick}",            "USER  #{nick}  ()  *  #{nick}"        ])          ping  =  server_events.select  {  |msg|  msg.command  ==  "PING"  }        pong  =  ping.map  {  |msg|  "PONG  "  +  msg.params.join("  ")  }          outgoing  =  process_user_commands(user_commands)          network_out  =  outgoing.merge(nick_and_user).merge(pong)          [network_out,  server_events]    end end Sunday, 7 April 13
  21. def  process_user_commands(user_commands)    user_commands.map  {  |cmd|        case

     cmd.action        when  nil            if  cmd.channel                "PRIVMSG  #{cmd.channel}  :#{cmd.argument}"            else                cmd.argument            end        when  :join            "JOIN  #{cmd.channel}"        when  :part            "PART  #{cmd.channel}"        when  :quit            "QUIT  :#{cmd.argument}"        else            nil        end    }.compact end Sunday, 7 April 13
  22. ChannelState  =  Struct.new(:joined_channels,  :current_index)   #  initial  state ChannelState.new([],  nil)

      #  after  joining  rubymanor ChannelState.new(["#rubymanor"],  0)   #  after  joining  cats  and  dogs ChannelState.new(["#rubymanor",  "#cats",  "#dogs"],  2)   #  after  typing  "/prev" ChannelState.new(["#rubymanor",  "#cats",  "#dogs"],  1) Sunday, 7 April 13
  23. class  ChannelState    def  update_channel_index(inc)        ChannelState.new(  

             joined_channels,            (channel_index  +  inc)  %  joined_channels.length)    end      def  join_channel(name)        new_list  =  joined_channels  |  [name]        new_idx    =  new_list.length-­‐1          ChannelState.new(new_list,  new_idx)    end      def  part_channel(name)        new_list  =  joined_channels  -­‐  [name]        new_idx    =  [channel_index,  new_list.length-­‐1].min          ChannelState.new(new_list,  new_idx)    end end Sunday, 7 April 13
  24. def  get_channel_state(user_commands,  server_events)    initial_state  =  ChannelState.new([],  nil)    

     manual_changes        =  get_manual_state_changes(user_commands)    automatic_changes  =  get_automatic_state_changes(server_events)    all_changes              =  manual_changes.merge(automatic_changes)      all_changes.scan(initial_state)  {  |state,  change|  ...  } end Sunday, 7 April 13
  25. def  get_manual_state_changes(user_commands)    user_commands        .select  {  |cmd|

               cmd.action  ==  :next  or  cmd.action  ==  :prev        }        .map  {  |cmd|            if  cmd.action  ==  :next                [:update_channel_index,  +1]            elsif  cmd.action  ==  :prev                [:update_channel_index,  -­‐1]            end                  }         end Sunday, 7 April 13
  26. def  get_automatic_state_changes(server_events)    server_events        .select  {  |event|

               event.command  ==  "JOIN"  or  event.command  ==  "PART"        }        .map  {  |event|            if  event.command  ==  "JOIN"                channel  =  event.params.first                [:join_channel,  channel]            elsif  event.command  ==  "PART"                channel  =  event.params.first                [:part_channel,  channel]            end        } end Sunday, 7 April 13
  27. all_changes.scan(initial_state)  {  |state,  change|    if  change.first  ==  :update_channel_index  

         state.update_channel_index(change.first,  change.last)    elsif  change.first  ==  :join_channel        state.join_channel(change.first,  change.last)    elsif  change.first  ==  :part_channel        state.part_channel(change.first,  change.last)    end } Sunday, 7 April 13
  28. def  get_manual_state_changes(user_commands)    user_commands        .select  {  |cmd|

               cmd.action  ==  :next  or  cmd.action  ==  :prev        }        .map  {  |cmd|            proc  {  |state|                if  cmd.action  ==  :next                    state.update_channel_index(+1)                elsif  cmd.action  ==  :prev                    state.update_channel_index(-­‐1)                end            }        } end Sunday, 7 April 13
  29. def  get_automatic_state_changes(server_events)    server_events        .select  {  |event|

               event.command  ==  "JOIN"  or  event.command  ==  "PART"        }        .map  {  |event|            proc  {  |state|                if  event.command  ==  "JOIN"                    state.join_channel(event.params.first)                elsif  event.command  ==  "PART"                    state.part_channel(event.params.first)                end              }              }       end Sunday, 7 April 13
  30. server_events_bus  =  Wick::Bus.new user_commands_bus  =  Wick::Bus.new   network_out,  server_events  =

       client.transform(network_in,  user_commands_bus)   user_out,  user_commands  =    ui.transform(user_in,  server_events_bus)   server_events_bus.consume!(server_events) user_commands_bus.consume!(user_commands) Sunday, 7 April 13