AshComputer

Hex.pmHex DocsLicense

A reactive computation DSL for Elixir, powered by Spark. Based on Lucassifoni/computer.

AshComputer provides a declarative way to define computational models that automatically update when their inputs change. Perfect for building calculators, form validations, reactive UIs, and any system requiring cascading computations.

Features

Installation

Add ash_computer to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_computer, "~> 0.6.0"}
  ]
end

Quick Start

Basic Calculator

defmodule MyApp.Calculator do
  use AshComputer

  computer :calculator do
    input :x do
      initial 10
    end

    input :y do
      initial 5
    end

    val :sum do
      compute fn %{x: x, y: y} -> x + y end
    end

    val :product do
      compute fn %{x: x, y: y} -> x * y end
    end
  end
end

# Use the calculator with Executor
executor =
  AshComputer.Executor.new()
  |> AshComputer.Executor.add_computer(MyApp.Calculator, :calculator)
  |> AshComputer.Executor.initialize()

values = AshComputer.Executor.current_values(executor, :calculator)
values[:sum]     # => 15
values[:product] # => 50

# Update inputs
executor =
  executor
  |> AshComputer.Executor.start_frame()
  |> AshComputer.Executor.set_input(:calculator, :x, 20)
  |> AshComputer.Executor.commit_frame()

values = AshComputer.Executor.current_values(executor, :calculator)
values[:sum]     # => 25
values[:product] # => 100

With Events

defmodule MyApp.TemperatureConverter do
  use AshComputer

  computer :converter do
    input :celsius do
      initial 0
      description "Temperature in Celsius"
    end

    val :fahrenheit do
      description "Temperature in Fahrenheit"
      compute fn %{celsius: c} -> c * 9/5 + 32 end
    end

    val :kelvin do
      description "Temperature in Kelvin"
      compute fn %{celsius: c} -> c + 273.15 end
    end

    event :set_from_fahrenheit do
      handle fn _values, %{fahrenheit: f} ->
        celsius = (f - 32) * 5/9
        %{celsius: celsius}
      end
    end
  end
end

# Use events with Executor
executor =
  AshComputer.Executor.new()
  |> AshComputer.Executor.add_computer(MyApp.TemperatureConverter, :converter)
  |> AshComputer.Executor.initialize()

executor = AshComputer.apply_event(
  MyApp.TemperatureConverter,
  :set_from_fahrenheit,
  executor,
  %{fahrenheit: 100}
)

values = AshComputer.Executor.current_values(executor, :converter)
values[:celsius] # => 37.78

LiveView Integration

AshComputer integrates seamlessly with Phoenix LiveView:

defmodule MyAppWeb.CalculatorLive do
  use Phoenix.LiveView
  use AshComputer.LiveView

  computer :calculator do
    input :amount do
      initial 100
    end

    input :tax_rate do
      initial 0.08
    end

    val :tax do
      compute fn %{amount: amount, tax_rate: rate} ->
        amount * rate
      end
    end

    val :total do
      compute fn %{amount: amount, tax: tax} ->
        amount + tax
      end
    end

    event :update_amount do
      handle fn _values, %{"value" => value} ->
        {amount, _} = Float.parse(value)
        %{amount: amount}
      end
    end
  end

  @impl true
  def mount(_params, _session, socket) do
    {:ok, mount_computers(socket)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <form phx-submit={event(:calculator, :update_amount)}>
        <label>
          Amount: $
          <input
            type="number"
            name="value"
            value={@calculator_amount}
            step="0.01"
          />
        </label>
        <button type="submit">Update</button>
      </form>

      <p>Tax (8%): $<%= Float.round(@calculator_tax, 2) %></p>
      <p>Total: $<%= Float.round(@calculator_total, 2) %></p>
    </div>
    """
  end
end

Compile-Time Safe Event References

AshComputer provides compile-time validation for event names in templates:

<!-- ✅ Compile-time safe - validates computer and event exist -->

<button phx-click={event(:calculator, :reset)}>Reset</button>
<form phx-submit={event(:calculator, :update_amount)}>...</form>

<!-- ❌ Old way - error-prone hardcoded strings -->

<button phx-click="calculator_reset">Reset</button>
<form phx-submit="calculator_update_amount">...</form>

If you reference a non-existent computer or event, you'll get a helpful compile-time error:

** (CompileError) Event :nonexistent not found in computer :calculator
Available events: [:update_amount, :reset]

This prevents typos and ensures your templates stay in sync with your computer definitions.

Custom Event Handlers and Manual Updates

While AshComputer automatically generates event handlers, you can also create custom handlers that update computer inputs manually:

defmodule MyAppWeb.DashboardLive do
  use Phoenix.LiveView
  use AshComputer.LiveView

  computer :sidebar do
    input :refresh_trigger do
      initial 0
    end

    input :filter do
      initial "all"
    end

    val :items do
      compute fn %{refresh_trigger: _trigger, filter: filter} ->
        # Fetch items from database based on filter
        fetch_items(filter)
      end
    end
  end

  @impl true
  def mount(_params, _session, socket) do
    {:ok, mount_computers(socket)}
  end

  # Custom handler that updates a single computer
  @impl true
  def handle_event("create_item", params, socket) do
    # Your business logic
    {:ok, _item} = create_item(params)

    # Trigger sidebar refresh by updating inputs
    updated_socket = update_computer_inputs(socket, :sidebar, %{
      refresh_trigger: System.monotonic_time()
    })

    {:noreply, updated_socket}
  end

  # Update multiple computers at once
  @impl true
  def handle_event("reset_all", _params, socket) do
    updated_socket = update_computers(socket, %{
      sidebar: %{filter: "all", refresh_trigger: 0},
      main_content: %{page: 1}
    })

    {:noreply, updated_socket}
  end
end

The update_computer_inputs/3 and update_computers/2 helper functions allow you to manually trigger recomputations from any custom event handler, making it easy to integrate AshComputer with existing business logic.

Advanced Features

Chained Computations

Values can depend on other computed values, creating computation chains:

computer :physics do
  input :mass do
    initial 10  # kg
  end

  input :velocity do
    initial 5  # m/s
  end

  val :momentum do
    compute fn %{mass: m, velocity: v} -> m * v end
  end

  val :kinetic_energy do
    compute fn %{mass: m, velocity: v} -> 0.5 * m * v * v end
  end

  val :de_broglie_wavelength do
    compute fn %{momentum: p} ->
      # h = Planck&#39;s constant
      6.626e-34 / p
    end
  end
end

Stateful Computers

For computations that need access to previous values:

computer :moving_average do
  stateful? true

  input :new_value do
    initial 0
  end

  val :average do
    compute fn %{new_value: new}, all_values ->
      previous = all_values[:average] || 0
      count = (all_values[:count] || 0) + 1
      ((previous * (count - 1)) + new) / count
    end
  end

  val :count do
    compute fn _deps, all_values ->
      (all_values[:count] || 0) + 1
    end
  end
end

Key Concepts

Why AshComputer?

Documentation

For detailed documentation, see HexDocs.

For AI assistance with this library, see usage-rules.md.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.