Skip to content

Instantly share code, notes, and snippets.

@zblanco
Created April 24, 2025 15:43
Show Gist options
  • Save zblanco/c20b9f894624e2d91e35971799283c26 to your computer and use it in GitHub Desktop.
Save zblanco/c20b9f894624e2d91e35971799283c26 to your computer and use it in GitHub Desktop.

Revisions

  1. zblanco created this gist Apr 24, 2025.
    512 changes: 512 additions & 0 deletions runic-demos.livemd
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,512 @@
    # Runic - Demos

    ```elixir
    Mix.install([
    {:kino, "~> 0.15.3"},
    {:runic, github: "zblanco/runic", branch: "zw/map"}
    ])
    ```

    ## Graph Visualization

    ```elixir
    defmodule Kino.Cytoscape do
    use Kino.JS

    def new(g, options \\ [])

    def new(%Graph{} = graph, options) do
    graph
    |> to_edgelist_json()
    |> new(options(options))
    end

    def new(edge_list, options) when is_list(options) do
    nodes =
    edge_list
    |> Enum.flat_map(fn %{data: edge} ->
    [
    %{data: %{id: "#{edge.source}", name: "#{edge.source}"}},
    %{data: %{id: "#{edge.target}", name: "#{edge.target}"}}
    ]
    end)
    |> Enum.uniq_by(& &1.data.id)

    new(nodes ++ edge_list, options(options))
    end

    def new(graph, options) do
    Kino.JS.new(__MODULE__, Map.put(options, :elements, graph))
    end

    defp options(:dag) do
    options = options([])

    dag_layout = %{
    name: "breadthfirst",
    fit: true,
    directed: true,
    avoidOverlap: true,
    spacingFactor: 1.0,
    grid: false,
    padding: 0,
    anime: true,
    circle: false,
    userZoomingEnabled: true
    }

    Map.put(options, :layout_options, dag_layout)
    end

    defp options(options) do
    cytoscape =
    options[:cytoscape] ||
    %{
    "zoomingEnabled" => true,
    "userZoomingEnabled" => true,
    "panningEnabled" => true,
    "userPanningEnabled" => true,
    "boxSelectionEnabled" => false
    }

    node_style =
    options[:node_style] ||
    %{
    "background-color" => "#ffffff",
    "font-size" => 14,
    "width" => 120,
    "shape" => "ellipse",
    "height" => 80,
    "text-wrap" => "wrap",
    "text-max-width" => 56,
    "color" => "#475569",
    "label" => "data(name)",
    "text-halign" => "center",
    "text-valign" => "center",
    "border-width" => 2,
    "border-color" => "#94a3b8",
    "border-opacity" => 0.5
    }

    edge_style =
    options[:edge_style] ||
    %{
    "width" => 2,
    "font-size" => 12,
    "label" => "data(label)",
    "line-color" => "#94a3b8",
    "target-arrow-color" => "#94a3b8",
    "target-arrow-shape" => "triangle",
    "curve-style" => "bezier"
    }

    layout_options =
    options[:layout_options] ||
    %{
    name: "cose",
    fit: true,
    directed: true,
    avoidOverlap: true,
    spacingFactor: 1.0,
    grid: false,
    padding: 0,
    anime: true,
    circle: false,
    userZoomingEnabled: true
    }

    %{
    cytoscape: cytoscape,
    node_style: node_style,
    edge_style: edge_style,
    layout_options: layout_options
    }
    end

    defp to_edgelist_json(graph) do
    vertices =
    graph
    |> Graph.vertices()
    |> Enum.map(fn v ->
    vertex_label = vertex_label(v)
    %{data: %{id: "#{vertex_label}", name: "#{vertex_label}"}}
    end)

    edges =
    graph
    |> Graph.edges()
    |> Enum.map(fn edge ->
    edge_label = edge_label(edge.label)

    v1 = vertex_label(edge.v1)
    v2 = vertex_label(edge.v2)

    %{
    data: %{
    id: "#{edge_label}-#{v1}#{v2}" |> :erlang.phash2(),
    label: "#{edge_label}",
    source: v1,
    target: v2
    }
    }
    end)

    vertices ++ edges
    end

    defp edge_label(label), do: label
    defp vertex_label({_, _, _} = ast), do: Macro.to_string(ast)

    defp vertex_label(%Runic.Workflow.Root{}), do: :root
    defp vertex_label(%{name: name}) when not is_nil(name), do: name

    defp vertex_label(%{__struct__: struct} = vertex),
    do: "#{to_string(struct)}#{:erlang.phash2(vertex)}"

    defp vertex_label(otherwise), do: otherwise

    asset "main.js" do
    """
    import "https://cdn.jsdelivr.net/npm/[email protected]/dist/cytoscape.min.js";
    export function init(ctx, cyto_data) {
    ctx.root.innerHTML = `<div id='cyto' style='width: 896px; height: 400px;'></div>`;
    var cy = cytoscape({
    ...cyto_data.cytoscape,
    ...{
    container: document.getElementById('cyto'),
    elements: cyto_data.elements,
    style: [
    {
    selector: 'node',
    style: cyto_data.node_style
    },
    {
    selector: 'edge',
    style: cyto_data.edge_style
    }
    ]
    }
    });
    cy.layout(cyto_data.layout_options).run();
    }
    """
    end
    end
    ```

    ## Intro

    Runic is a tool for modeling your workflows as data that can be composed together at runtime.

    Runic components can be integrated into a Runic.Workflow and evaluated lazily in concurrent contexts.

    Runic Workflows are a decorated dataflow graph (a DAG - "directed acyclic graph") capable of modeling rules, pipelines, and state machines and more.

    Basic data flow dependencies such as in a pipeline are modeled as `%Step{}` structs (nodes/vertices) in the graph with directed edges (arrows) between steps.

    A step can be thought of as a simple input -> output lambda function. e.g.

    ## Examples

    ```elixir
    require Runic

    step = Runic.step(fn x -> x + 1 end, name: :add_one)
    ```

    ```elixir
    workflow = Runic.workflow(
    name: "example pipeline workflow",
    steps: [
    Runic.step(fn x -> x + 1 end, name: :add_one),
    Runic.step(fn x -> x * 2 end, name: :times_two),
    Runic.step(fn x -> x - 1 end, name: :minus_one)
    ]
    )
    ```

    ```elixir
    workflow.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ```elixir
    alias Runic.Workflow

    wrk =
    workflow
    |> Workflow.react_until_satisfied(2)
    ```

    ```elixir
    wrk |> Workflow.raw_productions()
    ```

    ```elixir
    wrk |> Workflow.productions()
    ```

    ```elixir
    serial_pipeline =
    Runic.workflow(
    name: "serial_pipeline",
    steps: [
    {Runic.step(fn x -> x + 1 end, name: :add_one), [
    {Runic.step(fn x -> x * 2 end, name: :times_two), [
    Runic.step(fn x -> x - 1 end, name: :minus_one)
    ]}
    ]},
    Runic.step(fn x -> x + 5 end, name: :add_five)
    ]
    )
    ```

    ```elixir
    serial_pipeline.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ## Text Processing

    ```elixir
    defmodule TextProcessing do
    def tokenize(text) do
    text
    |> String.downcase()
    |> String.split(~r/[^[:alnum:]\-]/u, trim: true)
    end

    def count_words(list_of_words) do
    list_of_words
    |> Enum.reduce(Map.new(), fn word, map ->
    Map.update(map, word, 1, &(&1 + 1))
    end)
    end

    def count_uniques(word_count) do
    Enum.count(word_count)
    end

    def first_word(list_of_words) do
    List.first(list_of_words)
    end

    def last_word(list_of_words) do
    List.last(list_of_words)
    end
    end
    ```

    ```elixir
    import TextProcessing

    word_count =
    "anybody want a peanut?"
    |> tokenize()
    |> count_words()
    |> dbg()

    first_word =
    "anybody want a peanut?"
    |> tokenize()
    |> first_word()
    |> dbg()

    last_word =
    "anybody want a peanut?"
    |> tokenize()
    |> last_word()
    |> dbg()
    ```

    ```elixir
    text_processing_workflow =
    Runic.workflow(
    name: "basic text processing example",
    steps: [
    {Runic.step(&tokenize/1),
    [
    {Runic.step(&count_words/1),
    [
    Runic.step(&count_uniques/1)
    ]},
    Runic.step(&first_word/1),
    Runic.step(&last_word/1)
    ]}
    ]
    )
    ```

    ```elixir
    text_processing_workflow.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ```elixir
    text_processing_workflow
    |> Workflow.react_until_satisfied("anybody want a peanut?")
    |> Workflow.raw_productions()
    ```

    ## Joins

    ```elixir
    join_with_many_dependencies =
    Runic.workflow(
    name: "workflow with joins",
    steps: [
    {[Runic.step(fn num -> num * 2 end), Runic.step(fn num -> num * 3 end)],
    [
    Runic.step(fn num_1, num_2 -> num_1 * num_2 end),
    Runic.step(fn num_1, num_2 -> num_1 + num_2 end),
    Runic.step(fn num_1, num_2 -> num_2 - num_1 end)
    ]}
    ]
    )
    ```

    ```elixir
    join_with_many_dependencies.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ## Rules

    ```elixir
    rules_workflow =
    Runic.workflow(
    name: "a test workflow",
    rules: [
    Runic.rule(fn :foo -> :bar end, name: "foobar"),
    Runic.rule(
    if: fn lhs -> lhs == :potato end,
    do: fn rhs -> rhs == :tomato end,
    name: "tomato when potato"
    ),
    Runic.rule(
    fn item when is_integer(item) and item > 41 and item < 43 ->
    "the answer to life the universe and everything"
    end,
    name: "what about the question?"
    )
    ]
    )
    ```

    ```elixir
    rules_workflow.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ## State Machines

    ```elixir
    state_machine =
    Runic.state_machine(
    name: "transitional_factor",
    init: 0,
    reducer: fn
    num, state when is_integer(num) and state >= 0 and state < 10 -> state + num * 1
    num, state when is_integer(num) and state >= 10 and state < 20 -> state + num * 2
    num, state when is_integer(num) and state >= 20 and state < 30 -> state + num * 3
    _num, state -> state
    end
    )
    |> Runic.transmute()
    ```

    ```elixir
    state_machine.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ## Map

    ```elixir
    map_expression =
    Runic.workflow(
    name: "map test",
    steps: [
    {Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end),
    [
    Runic.map(
    {Runic.step(fn num -> num * 2 end),
    [
    Runic.step(fn num -> num + 1 end),
    Runic.step(fn num -> num + 4 end)
    ]}
    )
    ]}
    ]
    )
    ```

    ```elixir
    map_expression.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ## Reduce

    ```elixir
    reduce_example =
    Runic.workflow(
    name: "reduce test",
    steps: [
    {Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end),
    [
    {Runic.map(fn num -> num * 2 end),
    [
    Runic.reduce([], fn num, acc -> [num | acc] end)
    ]}
    ]}
    ]
    )
    ```

    ```elixir
    reduce_example.graph
    |> Kino.Cytoscape.new(:dag)
    ```

    ```elixir
    reduce_wrk =
    Runic.workflow(
    name: "reduce test",
    steps: [
    {Runic.step(fn _ -> 0..3 end),
    [
    Runic.map(
    {step(fn num -> num + 1 end),
    [
    Runic.step(fn num -> num + 4 end),
    Runic.step(fn num -> num + 2 end, name: :plus2)
    ]},
    name: "map"
    )
    ]}
    ]
    )

    reduce_wrk =
    Workflow.add(
    reduce_wrk,
    Runic.reduce(0, fn num, acc -> num + acc end, name: "reduce", map: "map"),
    to: :plus2
    )
    ```

    ```elixir
    reduce_wrk
    |> Workflow.react_until_satisfied(:anything)
    |> Workflow.productions()
    ```

    ```elixir
    reduce_wrk.graph
    |> Kino.Cytoscape.new(:dag)
    ```