View Source Handoff.DAG (Handoff v0.1.0)

Provides functionality for building and validating directed acyclic graphs (DAGs) of functions.

This module is the core of the Handoff library, allowing you to:

  1. Create empty computation graphs
  2. Add functions to the graph with their dependencies
  3. Validate the graph for correctness before execution

dag-structure

DAG Structure

A DAG in Handoff is represented as a map with:

examples

Examples

# Create a new DAG
dag = Handoff.DAG.new()

# Define functions
# Assumes MyPipelineFunctions module exists, e.g.:
# defmodule MyPipelineFunctions do
#   def transform_data(data_list), do: Enum.map(data_list, &(&1 * 2))
# end

source = %Handoff.Function{
  id: :data_source,
  args: [],
  code: &Elixir.Function.identity/1,
  extra_args: [[1, 2, 3, 4, 5]]
}

transform = %Handoff.Function{
  id: :transform,
  args: [:data_source],
  code: &MyPipelineFunctions.transform_data/1
}

aggregation = %Handoff.Function{
  id: :aggregate,
  args: [:transform],
  code: &Enum.sum/1
}

# Build the DAG
dag =
  dag
  |> Handoff.DAG.add_function(source)
  |> Handoff.DAG.add_function(transform)
  |> Handoff.DAG.add_function(aggregation)

# Validate the DAG
case Handoff.DAG.validate(dag) do
  :ok ->
    # DAG is valid and ready for execution
    IO.puts("DAG is valid")

  {:error, {:missing_dependencies, missing}} ->
    IO.puts("DAG has missing dependencies: #{inspect(missing)}")

  {:error, {:cycle_detected, cycle}} ->
    IO.puts("DAG contains a cycle at: #{inspect(cycle)}")
end

validation

Validation

The validate/1 function performs two critical checks:

  1. It ensures all dependencies reference existing functions
  2. It detects cycles in the graph using depth-first search

A valid DAG is required before execution.

Link to this section Summary

Functions

Adds a function to the DAG.

Creates a new empty DAG with a specified ID or generates a new one.

Validates that the DAG has no cycles and all dependencies exist.

Link to this section Functions

Link to this function

add_function(dag, function)

View Source

Adds a function to the DAG.

parameters

Parameters

  • dag: The current DAG structure
  • function: A Handoff.Function struct to add to the DAG

returns

Returns

  • Updated DAG with the function added

example

Example

dag =
  Handoff.DAG.new()
  |> Handoff.DAG.add_function(%Handoff.Function{
    id: :source,
    args: [],
    code: &:rand.uniform/1,
    extra_args: [100]
  })

Creates a new empty DAG with a specified ID or generates a new one.

parameters

Parameters

  • id: (Optional) The ID to assign to the DAG. If nil, a make_ref/0 will be used.

example

Example

dag_with_specific_id = Handoff.DAG.new("some-specific-id")
dag_with_generated_id = Handoff.DAG.new()

Validates that the DAG has no cycles and all dependencies exist.

returns

Returns

  • :ok if the DAG is valid
  • {:error, {:missing_function, id}} if references to an undefined function was found
  • {:error, {:cycle_detected, id}} if a cycle is found in the graph

example

Example

iex> dag = Handoff.DAG.new()
iex> dag = Handoff.DAG.add_function(dag, %Handoff.Function{id: :a, args: [], code: &Elixir.Function.identity/1, extra_args: [1]})
iex> dag = Handoff.DAG.add_function(dag, %Handoff.Function{id: :b, args: [:a], code: &Map.get/2, extra_args: [:a]})
iex> Handoff.DAG.validate(dag)
:ok

error-cases

Error cases

iex> dag = Handoff.DAG.new()
iex> dag = Handoff.DAG.add_function(dag, %Handoff.Function{id: :a, args: [:b], code: &Elixir.Function.identity/1})
iex> Handoff.DAG.validate(dag)
{:error, {:missing_function, :b}}

iex> dag = Handoff.DAG.new()
iex> dag = Handoff.DAG.add_function(dag, %Handoff.Function{id: :a, args: [:b], code: &Map.get/2, extra_args: [:b]})
iex> dag = Handoff.DAG.add_function(dag, %Handoff.Function{id: :b, args: [:a], code: &Map.get/2, extra_args: [:a]})
iex> {:error, {:cyclic_dependency, cycle}} = Handoff.DAG.validate(dag)
iex> Enum.sort(cycle)
[:a, :b]