View Source Skitter.DSL.Component (Skitter v0.5.0)

Callback and Component definition DSL.

This module offers macros to define component modules and callbacks. Please refer to the documentation of defcomponent/3.

Link to this section Summary

Functions

Updates the current state.

Emit value to port

Emit several values to port

Obtain the component configuration.

Define a callback.

Define a component module.

Read the current value of a field stored in state.

Obtain the current state.

Defines the initial state of a component.

Creates an initial struct-based state for a component.

Link to this section Functions

Updates the current state.

This macro should only be used inside the body of defcb/2. It updates the current value of the component state to the provided value.

This macro can be used in two ways: it can be used to update the component state or a field of the component state. The latter option can only be used if the state of the component is a struct (i.e. if the intial state has been defined using state_struct/1). The former options modifies the component state as a whole, the second option only modifies the value of the provided field stored in the component state.

Examples

defcomponent WriteExample do
  defcb write(), do: state <~ :foo
end
iex> Component.call(WriteExample, :write, nil, nil, []).state
:foo
defcomponent FieldWriteExample do
  state_struct [:field]
  defcb write(), do: field <~ :bar
end
iex> Component.call(FieldWriteExample, :write, %FieldWriteExample{field: :foo}, nil, []).state.field
:bar
defcomponent WrongFieldWriteExample do
  fields [:field]
  defcb write(), do: doesnotexist <~ :bar
end
iex> Component.call(WrongFieldWriteExample, :write, %WrongFieldWriteExample{field: :foo}, nil, [])
** (KeyError) key :doesnotexist not found in: %Skitter.DSL.ComponentTest.WrongFieldWriteExample{field: :foo}

Emit value to port

This macro is used to specify value should be emitted on port. This means that value will be sent to any components downstream of the current component. This macro should only be used inside the body of defcb/2. If a previous value was specified for port, it is overridden.

Examples

defcomponent SingleEmitExample do
  defcb emit(value) do
    value ~> some_port
    :foo ~> some_other_port
  end
end
iex> Component.call(SingleEmitExample, :emit, [:bar]).emit
[some_other_port: [:foo], some_port: [:bar]]

Emit several values to port

This macro works like ~>/2, but emits several output values to the port instead of a single value. Each value in the provided Enumerable.t/0 will be sent to downstream components individually.

Examples

defcomponent MultiEmitExample do
  defcb emit(value) do
    value ~> some_port
    [:foo, :bar] ~>> some_other_port
  end
end
iex> Component.call(MultiEmitExample, :emit, [:bar]).emit
[some_other_port: [:foo, :bar], some_port: [:bar]]

Obtain the component configuration.

This macro reads the current value of the configuration passed to the component callback when it was called. It should only be used inside the body of defcb/2.

Examples

defcomponent ConfigExample do
  defcb read(), do: config()
end
iex> Component.call(ConfigExample, :read, []).result
nil

iex> Component.call(ConfigExample, :read, :config, []).result
:config

iex> Component.call(ConfigExample, :read, :state, :config, []).result
:config
Link to this macro

defcb(signature, list)

View Source (macro)

Define a callback.

This macro is used to define a callback function. Using this macro, a callback can be defined similar to a regular procedure. Inside the body of the procedure, ~>/2, ~>>/2 <~/2 and sigil_f/2 can be used to access the state and to emit output. The macro ensures:

Note that, under the hood, defcb/2 generates a regular elixir function. Therefore, pattern matching may still be used in the argument list of the callback. Attributes such as @doc may also be used as usual.

Examples

defcomponent CbExample do
  defcb simple(), do: nil
  defcb arguments(arg1, arg2), do: arg1 + arg2
  defcb state(), do: counter <~ (~f{counter} + 1)
  defcb emit_single(), do: ~D[1991-12-08] ~> out_port
  defcb emit_multi(), do: [~D[1991-12-08], ~D[2021-07-08]] ~>> out_port
end
iex> Component.callbacks(CbExample)
#MapSet<[arguments: 2, emit_multi: 0, emit_single: 0, simple: 0, state: 0]>

iex> Component.callback_info(CbExample, :simple, 0)
%Info{read?: false, write?: false, emit?: false}

iex> Component.callback_info(CbExample, :arguments, 2)
%Info{read?: false, write?: false, emit?: false}

iex> Component.callback_info(CbExample, :state, 0)
%Info{read?: true, write?: true, emit?: false}

iex> Component.callback_info(CbExample, :emit_single, 0)
%Info{read?: false, write?: false, emit?: true}

iex> Component.callback_info(CbExample, :emit_multi, 0)
%Info{read?: false, write?: false, emit?: true}

iex> Component.call(CbExample, :simple, %{}, nil, [])
%Result{result: nil, emit: [], state: %{}}

iex> Component.call(CbExample, :arguments, %{}, nil, [10, 20])
%Result{result: 30, emit: [], state: %{}}

iex> Component.call(CbExample, :state, %{counter: 10, other: :foo}, nil, [])
%Result{result: nil, emit: [], state: %{counter: 11, other: :foo}}

iex> Component.call(CbExample, :emit_single, %{}, nil, [])
%Result{result: nil, emit: [out_port: [~D[1991-12-08]]], state: %{}}

iex> Component.call(CbExample, :emit_multi, %{}, nil, [])
%Result{result: nil, emit: [out_port: [~D[1991-12-08], ~D[2021-07-08]]], state: %{}}
Link to this macro

defcomponent(name, opts \\ [], list)

View Source (macro)

Define a component module.

This macro is used to define a component module. Using this macro, a component can be defined similar to a normal module. The macro will enable the use of defcb/2 and provides implementations for Skitter.Component._sk_component_info/1, Skitter.Component._sk_component_initial_state/0, Skitter.Component._sk_callbacks/0 and Skitter.Component._sk_callback_info/2.

Component strategy and ports

The component Strategy and its in -and out ports can be defined in the header of the component declaration as follows:

iex> defcomponent SignatureExample, in: [a, b, c], out: [y, z], strategy: SomeStrategy do
...> end
iex> Component.strategy(SignatureExample)
SomeStrategy
iex> Component.in_ports(SignatureExample)
[:a, :b, :c]
iex> Component.out_ports(SignatureExample)
[:y, :z]

If a component has no in, or out ports, they can be omitted from the component's header. Furthermore, if the component only has a single in or out port, the list notation can be omitted:

iex> defcomponent PortExample, in: a do
...> end
iex> Component.in_ports(PortExample)
[:a]
iex> Component.out_ports(PortExample)
[]

The strategy may be omitted. In this case, a strategy must be provided when the defined component is embedded inside a workflow. If this is not done, an error will be raised when the workflow is deployed.

Examples

defcomponent Average, in: value, out: current do
  state_struct total: 0, count: 0

  defcb react(value) do
    total <~ ~f{total} + value
    count <~ ~f{count} + 1

    ~f{total} / ~f{count} ~> current
  end
end
iex> Component.in_ports(Average)
[:value]
iex> Component.out_ports(Average)
[:current]


iex> Component.strategy(Average)
nil

iex> Component.call(Average, :react, [10])
%Result{result: nil, emit: [current: [10.0]], state: %Average{count: 1, total: 10}}

iex> Component.call(Average, :react, %Average{count: 1, total: 10}, nil, [10])
%Result{result: nil, emit: [current: [10.0]], state: %Average{count: 2, total: 20}}

Documentation

When writing documentation for a component, @componentdoc can be used instead of the usual @moduledoc. When this is done, this macro will automatically add additional information about the component to the generated documentation.

Link to this macro

generate_moduledoc(env)

View Source (macro)
Link to this macro

sigil_f(arg, _)

View Source (macro)

Read the current value of a field stored in state.

This macro expects that the current component state is a struct (i.e. it expects a component that uses state_struct/1), and reads the current value of field from the struct.

This macro should only be used inside the body of defcb/2.

Examples

defcomponent FieldReadExample do
  state_struct field: nil
  defcb read(), do: ~f{field}
end
iex> Component.call(FieldReadExample, :read, %FieldReadExample{field: 5}, nil, []).result
5

iex> Component.call(FieldReadExample, :read, %FieldReadExample{field: :foo}, nil, []).result
:foo

Obtain the current state.

This macro reads the current value of the state passed to the component callback when it was called. It should only be used inside the body of defcb/2.

Examples

defcomponent ReadExample do
  state 0
  defcb read(), do: state()
end
iex> Component.call(ReadExample, :read, []).result
0

iex> Component.call(ReadExample, :read, :state, nil, []).result
:state

iex> Component.call(ReadExample, :read, :state, nil, []).result
:state
Link to this macro

state(initial_state)

View Source (macro)

Defines the initial state of a component.

This macro is used to define the initial state of a component. This state is passed to every called callback when no state is provided by the component's strategy. When this macro is not used, the initial state of a component is nil.

Internally, this macro generates a definition of Skitter.Component._sk_component_initial_state/0.

Examples

defcomponent NoStateExample do
  defcb return_state, do: state()
end

defcomponent StateExample do
  state 0
  defcb return_state, do: state()
end
iex> Component.initial_state(NoStateExample)
nil

iex> Component.initial_state(StateExample)
0

iex> Component.call(NoStateExample, :return_state, []).state
nil

iex> Component.call(StateExample, :return_state, []).state
0

iex> Component.call(NoStateExample, :return_state, :some_state, nil, []).state
:some_state

iex> Component.call(StateExample, :return_state, :some_state, nil, []).state
:some_state
Link to this macro

state_struct(fields)

View Source (macro)

Creates an initial struct-based state for a component.

In Elixir, it is common to use a struct to store structured information. Therefore, when a component manages a complex state, it often defines a struct and uses this struct as the initial state of the component. Afterwards, the state of the component is updated when it reacts to incoming data:

defcomponent Average, in: value, out: current do
  defstruct [total: 0, count: 0]
  state %__MODULE__{}

  defcb react(val) do
    state <~ %{state() | count: state().count + 1}
    state <~ %{state() | total: state().total + val}
    state().total / state().count ~> current
  end
end

In order to streamline the use of this pattern, this macro defines a struct and uses this struct as the initial state of the component. Moreover, the sigil_f/2 and ~>/2 macros are designed to be used with structs, enabling them to read the state and update it:

defcomponent Average, in: value, out: current do
  state_struct total: 0, count: 0

  defcb react(val) do
    count <~ ~f{count} + 1
    total <~ ~f{total} + val
    ~f{total} / ~f{count} ~> current
  end
end

The second example generates the code shown in the first example.

Examples

iex> Component.initial_state(Average)
%Average{total: 0, count: 0}