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

Simpler Cycling with Megadrivers

Simpler Cycling with Megadrivers

Talk I gave for CycleConf 2017 in Stockholm, 22 Apr 2017

Justin Woo

April 22, 2017
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. Simpler Cycling with
    Megadrivers
    Justin Woo
    22 Apr 17

    View full-size slide

  2. Quick review
    “What is Cycle.js?”
    “A functional and reactive JavaScript framework for predictable code”
    Nice utility “framework” for building my apps as a cycle of streams
    Nicely separates what effects happen from my state
    Able to use power of reactive programming (flatMap!!!, etc.)
    No single/chain reaction action dispatch queue

    View full-size slide

  3. Typical Setup
    Main function:
    main(sink: A extends { [key: string]: any }) =>
    B extends { [key: string]: Observable }
    I didn’t know that second part until last late year! (Cycle.js user since 2015)

    View full-size slide

  4. The “Problem”
    Quite dynamic
    string keys? (or very hacky type constructor tricks in TS2/Flow32+)
    Sink/driver matching by string key is annoying & manual
    Feels like type annotations take a lot of work to add
    I like having one stream that I can split/transform however I want
    In dev, I want to log all driver commands easily

    View full-size slide

  5. First pass “solution”
    What if we just used a “tuple”?
    We know which position at which sink/driver interaction is expected
    Easy enough to type, I guess (need redundant type sigs)
    function run(
    main: (sources: [A1]) => [A2],
    drivers: [(sink: A2) => A1])
    function run(
    main: (sources: [A1, B1]) => [A2, B2],
    drivers: [(sink: A2) => A1, (sink: B2) => B1])

    View full-size slide

  6. But it falls short...
    Redundant type definitions are not fun
    Grows linearly and is ugly!!!
    Tuples aren’t fun to work (indices have no meaning)
    Separate streams with driver per command stream
    I want to access whichever, whenever!

    View full-size slide

  7. Rethinking the problem
    One stream that I want to flatMap as I wish
    Cram everything together!
    Since return value main function is a stream…
    Algebraic Data Type approximation!
    People now very familiar with “union types” anyhow
    Using “type” tagged objects
    (At the very least, 100% JSON compatible)

    View full-size slide

  8. And so...
    My new run function is quite simple:
    function run(
    main: (source: A) => Stream,
    driver: (sink: Stream) => A)
    Where
    A is my source from my “megadriver”
    B is my stream of commands to the world

    View full-size slide

  9. function run(main: (source: A) => Stream, driver: (sink: Stream) => A) {
    const stream = xs.create(); // makeSubject
    const observer = /* censored */
    // feed subject to driver at first
    const source = driver(stream);
    main(source).addListener(observer); // streamSubscribe
    return () => stream.removeListener(observer);
    }
    Simple enough!
    Definition of my “custom”run function

    View full-size slide

  10. Defining Command
    Say I wanted to
    1. Render a VTree to DOM
    2. Print things to console.log
    Then we need to
    1. Create some tagged objects for each
    command
    2. Create a union type for these
    type Command
    = DOMCommand
    | ConsoleCommand
    type DOMCommand = {
    type: 'DOMCommand',
    payload: { node: VNode } };
    type ConsoleCommand = {
    type: 'ConsoleCommand',
    payload: { text: string } };

    View full-size slide

  11. Defining Source
    Same as usual, where if I had
    1. A DOM source
    2. A timer source
    Then we just need to type up the annotation
    interface Source {
    dom: DOMSource,
    timer: Stream
    }

    View full-size slide

  12. Driver
    function driver(sink: Stream):
    Source {
    sink.map(cmd =>
    cmd.type === 'ConsoleCommand'
    ? xs.of(cmd.payload.text)
    : xs.empty()
    ).flatten().addListener({
    next: x => console.log(x),
    // ...
    // could also just handle the cases in your
    subscriber
    const domDriver = makeDOMDriver('#app');
    const dom = domDriver(sink.map(cmd =>
    cmd.type === 'DOMCommand'
    ? xs.of(cmd.payload.node)
    : xs.empty()
    ).flatten(), streamAdapter);
    return {
    dom,
    timer: xs.periodic(1000) }}

    View full-size slide

  13. function main(source: Source): Stream {
    const inputs = source.dom.select('.field')
    .events('input')
    .map(ev => (ev.target as HTMLInputElement)
    .value)
    .startWith('');
    const counts = source.timer;
    const vnodes = xs.combine(inputs, counts)
    .map(([name, count]) =>
    // typical vtree code
    .map(vnode => createDOMCommand(vnode));
    Main
    const strings = xs
    .combine(inputs, counts)
    .map(([name, count]) =>
    `${name}: ${count}`)
    .map(string =>
    createConsoleCommand(string));
    return xs.merge(vnodes, strings); }
    function createDOMCommand(node: VNode): Command {
    return {type: 'DOMCommand', payload: {node}};}
    function createConsoleCommand(text: string): Command {
    return {type: 'ConsoleCommand', payload: {text}};}

    View full-size slide

  14. Result
    All that just to get this rinky-dink thing →
    But at least now everything is simpler!
    Less work to annotate
    TS & Flow can do everything for us
    We can even log out all of the commands for
    easy debugging, Redux style

    View full-size slide

  15. Conclusion 1
    Less is more!
    No more string-key matching between sink and driver input
    Tagged object unions are easy to work with
    This makes TS and Flow more helpful as a result
    No need for complicated keyOf/valueOf type constructors

    View full-size slide

  16. Enter… my real goal
    Wanted simple way to use Cycle.js in Purescript
    Dynamic records are too much of a pain
    Now I have this megadriver approach
    Integrating JS code through FFI: easy peasy
    Want to port my ex-Cycle Purescript app to Purescript-Cycle!

    View full-size slide

  17. Functional programming language targeting Javascript (Haskell-esque)
    Conforms to JS: strictly evaluated, no runtime (pretty small output as a result)
    Already in production use at many companies
    In “production” on my VPS for 1 year
    Great Foreign Function Interface
    Write as little or much Javascript as you want!
    What is Purescript?

    View full-size slide

  18. Purescript-Cycle-Run
    type Dispose e = Unit -> Eff e Unit
    run :: forall e a b.
    (a -> Stream b) ->
    (Stream b -> Eff e a) ->
    Eff e (Dispose e)
    run = runFn2 _run
    foreign import _run :: forall e a b. Fn2
    (a -> Stream b)
    (Stream b -> Eff e a)
    (Eff e (Dispose e))
    ...and 23 lines of FFI JS to handle effs:
    Eff: thunked functions e.g. _driver(sink)()

    View full-size slide

  19. FFI Code
    var Cycle = require('@cycle/xstream-run');
    exports._run = function (_main, _driver) {
    return function () {
    function main (sources) {
    return {
    main: _main(sources.main)
    };
    }
    function driver (sink) {
    return _driver(sink)();
    }
    var dispose = Cycle.run(main, {
    main: driver
    });
    return function (unit) {
    return function () {
    dispose();
    };
    };};};

    View full-size slide

  20. In use - Telegram Bot
    Telegram bot for two things:
    1. Check torrents on a timer or per
    request “get”
    2. Respond to location messages
    with directions home using HSL
    API
    data Query
    = ScrapeRequest
    | TimerRequest
    | LastTrainRequest RequestWithOrigin
    | QueueMessage Result
    data Command
    = ScrapeCommand RequestOrigin
    | LastTrainCommand RequestWithOrigin
    | SendMessage Result
    | Info String

    View full-size slide

  21. Main
    main_ :: Stream Query -> Stream Command
    main_ queries = inner <$> queries
    where
    inner = case _ of
    TimerRequest -> ScrapeCommand Timer
    ScrapeRequest -> ScrapeCommand User
    LastTrainRequest req -> LastTrainCommand req
    QueueMessage result@{origin, output} -> do
    case Tuple origin
    (indexOf
    (Pattern "nothing new") output) of
    Tuple Timer (Just _) ->
    Info "timer found nothing"
    _ ->
    SendMessage result

    View full-size slide

  22. Driver - setup
    driver :: forall e.
    Config -> Stream Command
    -> Eff (MyEffects e) (Stream Query)
    driver
    (Config
    { token
    , torscraperPath
    , lastTrainHomePath
    , master
    })
    commands = do
    bot <- connect token
    timer <- periodic (60 * 60 * 1000)
    scrapeRequests <- getMessages bot
    lastTrainRequests <- locationMessages bot

    View full-size slide

  23. Driver - handle commands
    results <- (commands <|> pure (ScrapeCommand Timer)) `switchMapEff` \command -> do
    _ <- logShow command
    case command of
    ScrapeCommand origin ->
    fromAff $ runTorscraper torscraperPath {origin, id: master}
    LastTrainCommand location ->
    fromAff $ runLastTrainHome lastTrainHomePath location
    SendMessage result -> do
    _ <- sendMessage' bot result
    pure mempty
    _ -> pure mempty

    View full-size slide

  24. Driver - gather queries and return
    pure
    $ TimerRequest <$ timer
    <|> ScrapeRequest <$ scrapeRequests
    <|> LastTrainRequest <<< {id: master, origin: User, location: _}
    <$> lastTrainRequests
    <|> QueueMessage <$> results

    View full-size slide

  25. That’s it!
    main_ :: Stream Query -> Stream Command
    driver :: forall e. Config -> Stream Command -> Eff (MyEffects e) (Stream Query)
    main = launchAff $ do
    runExcept <$> getConfig >>=
    case _ of
    Left e ->
    AffC.log $ "config.json is malformed: " <> show e
    Right config ->
    liftEff <<< void $ run main_ (driver config)

    View full-size slide

  26. Conclusion 2
    Megadriver idea is easy to type with ADTs
    Using Cycle from Purescript is fun
    My bot works!

    View full-size slide

  27. Thanks!
    Repos:
    ● https://github.com/justinwoo/cyclejs-megadriver-demo/
    ● https://github.com/justinwoo/purescript-cycle-run
    ● https://github.com/justinwoo/simple-rpc-telegram-bot/
    Some history:
    Type check main function and drivers in run() https://github.com/cyclejs/cyclejs/issues/404
    Simplify drivers by making it a hodgepodge? https://github.com/cyclejs/cyclejs/issues/432

    View full-size slide