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

DDD: Data Driven Development

DDD: Data Driven Development

Some patterns of functional programming in Elixir. #elixirfest

さっちゃん

June 01, 2019
Tweet

More Decks by さっちゃん

Other Decks in Programming

Transcript

  1. DDD: Data Driven
    Development
    Some patterns of functional programming in Elixir.

    View Slide

  2. .。oO(さっちゃんですよヾ(〃l ̲ l)ノ゙ ☆)

    View Slide

  3. View Slide

  4. I'm creating a realtime fighting game server in Elixir/Phoenix.

    View Slide

  5. The game server
    Our game is nearly TCG (trading card game; PvP & PvE).
    Very complex game rule.
    Should view the complex rule to the users.
    Have many kind of cards.
    The calculation should be fast.

    View Slide

  6. How to develop Elixir servers
    https://speakerdeck.com/ne̲sachirou/sutetohurudeda‑gui‑mo‑
    akusesufalsearusoft‑realtimenagemusabawoeasynitukuru

    View Slide

  7. How to scale PubSub by Redis
    https://speakerdeck.com/ne̲sachirou/phoenix‑at‑scale

    View Slide

  8. How to deploy Elixir servers
    https://speakerdeck.com/ne̲sachirou/elixir‑on‑containers

    View Slide

  9. OK. We know about how to develop & operate realtime server
    with Elixir/Phoenix.

    View Slide

  10. But, how to write such a complex game rules in Elixir?

    View Slide

  11. Features of Elixir
    Fault tolerance, soft‑realtime, hot code swap.
    Parallelism, distributed system.
    Functional programming, immutable data.

    View Slide

  12. The main building blocks of Erlang systems are spawn_link/1
    &
    send/2
    .

    View Slide

  13. spawn_link/1
    &
    send/2
    Pros:
    Easy to make concurrency.
    Have shared & implicit state.

    View Slide

  14. spawn_link/1
    &
    send/2
    Cons:
    Message passing is slow.
    Message queue becomes a bottleneck.
    Cause data copy (e.g. ETS).
    FP (functional programming) has no cons like this.

    View Slide

  15. FP of Elixir
    Pros:
    Performant. No message passing. No message queue.

    View Slide

  16. FP of Elixir
    Cons:
    All state is explicit.

    View Slide

  17. All state is explicit.
    Cons?

    View Slide

  18. We MUST write ALL state at function arguments & returns.

    View Slide

  19. def f(a, b, c, d, e, f, g, h, i, j, k, l, m, n) do
    {a, b, c, d, e, f, g, h, i, j, k, l, m, n}
    end

    View Slide

  20. def f(a, %{b: b, c: %{d: d, e: e}, f: f} = s1, g, %{h: h} = s2) do
    {g, s1, s2} = g(g, s1, s2)
    s1 = update_in(s1.c.d, &(&1 + 1))
    {a + g, d, s1, s2}
    end

    View Slide

  21. FP cons
    Function arguments & returns are not extensible.
    Composed functions are not extensible.

    View Slide

  22. FP primers
    They resolve the cons.
    ML, Haskell
    Lisp, Clojure
    React/Redux

    View Slide

  23. FP primers
    But they are diffent from Elixir.
    ML, Haskell : Have powerful type‑level cal.
    Lisp, Clojure : Have powerful macro & implicit state.
    React/Redux : Isn't immutable.

    View Slide

  24. How can we survive FP in Elixir?

    View Slide

  25. FP patterns

    View Slide

  26. Complexity shold grow.
    Function grows to chaos, because a function encapsulate it's
    source code.
    Data can grow.
    Data is composable & extensible, because data is explicit.

    View Slide

  27. Express your problem as data.

    View Slide

  28. Basic data structures

    View Slide

  29. Just a value
    atom
    float
    fun
    integer
    pid
    port
    reference

    View Slide

  30. binary
    <<_::_*n>>
    Expression for other system.

    View Slide

  31. tuple
    {a, b}
    The position have a meaning.

    View Slide

  32. list
    [x | xs]
    [a: a, b: b]
    'ab'
    Recrusive.

    View Slide

  33. map
    %{a: a, b: b}
    [a: a, b: b]
    %A{a: a, b: b}
    Maps a key to a value.

    View Slide

  34. tuple or map?
    Only map is extensible.
    # NG
    {a, b} = {a, b, c}
    # OK
    %{a: a, b: b} = %{a: a, b: b, c: c}

    View Slide

  35. Map is composable,
    %{a: a, b: b} == Map.merge(%{a: a}, %{b: b})
    but it lacks the data about what it's made from.
    %{a: a, b: b} == Map.merge(%{}, %{a: a, b: b})
    %{a: a, b: b} == Map.merge(%{a: a}, %{b: b})
    So map isn't decomposable.

    View Slide

  36. Don't read & modify maps or structs directly.
    # Sometimes NG
    %A{a: 0}.a
    %{a: 1 | %A{a: 0}}
    # OK
    A.a(%A{a: 0})
    get_in(%A{a: 0}[:a])
    A.put_a(%A{a: 0}, 1)
    put_in(%A{a: 0}[:a], 1)

    View Slide

  37. FP Patterns
    I'm using.
    Protocol
    Event sourcing
    Linguistic tree
    Composable context

    View Slide

  38. Protocol
    is a good part of Elixir.

    View Slide

  39. Protocol is meta‑level function that maps data to function.
    Protocol(T) : T -> fun(T, ...)
    Protocol is extensible, just like a map.
    Protocols ‑ Elixir https://elixir‑lang.org/getting‑
    started/protocols.html
    Protocols ∙ Elixir School
    https://elixirschool.com/en/lessons/advanced/protocols/

    View Slide

  40. We can list the current domain of the protocol.
    (Works like STI (single table inheritance) of ActiveRecord/Rails.)
    defprotocol P do
    end
    defmodule A do
    defstruct []
    def type, do: "a"
    defimpl P do
    end
    end
    data = %{type: "a"}
    ExampleProtocol
    |> Protocol.extract_impls([:code.lib_dir(:example, :ebin)])
    |> Enum.find(&(&1.type() == data.type))
    |> struct(data)

    View Slide

  41. Event sourcing
    Composed function is not extensible. Put a queue between
    functions.
    # Not this
    f2(f1(data))
    data |> f1 |> f2
    # This
    data -> queue -> f1 -> queue -> f2
    a.k.a. FRP (functional reactive programming)
    cf. Redux, rx, clojure/core.async

    View Slide

  42. Queue can have an environment.
    Dispatch (relates to protocol):
    %A{} -> queue -> fa
    %B{} -> queue -> fb

    View Slide

  43. Queue can have an environment.
    Priority:
    [p2, p3] -> queue -> f(p2) -> p1 -> queue -> f(p1) -> queue -> f(p3)

    View Slide

  44. Queue can have an environment.
    Modify:
    a1 -> queue(a1 -> a2) -> f(a2)

    View Slide

  45. Linguistic tree
    The slogan is “Trees arise everywhere.” :)
    Formal linguistics (& linguistic AI) have many patterns to treat
    complex data.
    Ex. I'm using:
    α‑Movement
    Behaviour tree
    GTTM (Generative Theory of Tonal Music)

    View Slide

  46. Composable context
    What "context" means?
    State.
    Environment.

    View Slide

  47. Context have a state.
    @impl Context
    def init(args), do: state
    context = new(ContextA, args)
    Context can perform effects.
    @impl Context
    def handle({:effect_a, args}, state, context),
    do: {:reply, response, state, []}
    def handle(_, _, _), do: :ignore
    {response, context} = perform(context, {:effect_a, args})
    cf. Algebraic Effects‑poi

    View Slide

  48. Context can be composed.
    context_ab = new() |> put(ContextA, args) |> put(ContextB, args)
    context_ab = add(put(new(), ContextA, args), put(new(), ContextB, args))
    This works like a decomposable map.

    View Slide

  49. Contexts is a noncommutative monoid, just like a keyword list.
    add(ctx_a, ctx_b) == ctx_ab
    add(add(ctx_a, ctx_b), ctx_c) ==
    add(ctx_a, add(ctx_b, ctx_c))
    add(ctx_a, ctx_b) != add(ctx_b, ctx_a)
    add(ctx_a, new) == add(new, ctx_a) == ctx_a

    View Slide