Sunday, June 21, 2015

On a component's behaviour

I have been busy implementing more of the commands that are part of the FBP Network protocol which is described here. If an FBP runtime speaks this protocol then one is able to use the neat development environment that has been developed by the NoFlo folks. You can run their interface locally, as a node.js program or you can use the online version: app.flowhub.io. I've been able to use the online version to create a small graph that I can then execute.

The current implementation of ElixirFBP has its components hard-wired into the runtime. This is, of course, temporary. I eventually want the runtime to be able to find and load components dynamically. NoFlo has the idea of a ComponentLoader for their node.js implementation:
"Node.js version of the Component Loader finds components and graphs by traversing the NPM dependency tree from a given root directory on the file system."
Any component-based framework, such as ElixirFBP, becomes generally useful if it is possible to construct components that can be "plugged" into the framework so long as the components obey rules as defined by an API. In other words, the component must behave properly. Elixir and Erlang have the concept of a behaviour, basically the specification of an api that a Elixir/Erlang module must implement. Let's see how this might possibly be useful.

A typical ElixirFBP component, Math.Add looks like the following:
defmodule Math.Add do
  def description, do: "Add two integers"
  def inports, do: [addend: :integer, augend: :integer]
  def outports, do: [sum: :integer]

  def loop(augend, addend, sum) do
    receive do
      {:augend, value} when addend != nil ->
        sum = ElixirFBP.Component.send_ip(sum, addend + value)
        loop(nil, nil, sum)
      {:augend, value} ->
        loop(value, addend, sum)
      {:addend, value} when augend != nil ->
        sum = ElixirFBP.Component.send_ip(sum, value + augend)
        loop(nil, nil, sum)
      {:addend, value} ->
        loop(augend, value, sum)
    end
  end
end

Notice the four functions definitions: description, inports, outports, and loop. With the exception of the description function, they must all be present for this component to operate correctly. In the process of building an FBP graph and connecting components, ElixirFBP, given the module name of a component, can retrieve the inports and outports of a component by executing the following code:
    component_inports = elem(Code.eval_string(component <> ".inports"), 0)
    component_outports = elem(Code.eval_string(component <> ".outports"), 0)
Eventually, this component is started by spawning a process:
process_pid = spawn(module, :loop, inport_args ++ outport_args)
These three functions. at least at this point in ElixirFBP's development, define the API for an ElixirFBP component or, in Elixir/Erlang terms, they describe the behaviour of the component. In Elixir, a behaviour is specified using the Behaviour macros. Here is what the ElxirFBP Component behaviour might look like:
defmodule Component do
  use Behaviour

  defcallback inports() :: [ElixirAtom: ElixirAtom]
  defcallback outports() :: [ElixirAtom: ElixirAtom]
end
A component must implement two functions, inports() and outports() that both will return a list of tuples. In Elixir, if the tuples are structured a certain way, the list can be treated as a Keyword list.

It was at this point that I realized that the way I have been defining the loop function of a component was not general enough. If you look at the Math.Add component above, you'll see that the loop function takes three arguments. I could define a loop behaviour callback that accepts three arguments but there are two problems: Which arguments are for the in ports and which are for the out ports? And, of course, a component can have any number of in and out ports. A solution is to generalize the loop function to accept two maps: one for the in ports and one for the out ports. Its behaviour could look like:
  defcallback loop(%{}, %{}) :: any
The component's loop function must have two arguments both of which are Elixir Maps. This change means that the way an ElixirFBP component is written needs to change. Here's what Math.Add looks like when rewritten to conform to the Component behaviour:
defmodule Math.Add do
  def description, do: "Add two integers"
  def inports, do: [addend: :integer, augend: :integer]
  def outports, do: [sum: :integer]

  def loop(inports, outports) do
    %{:augend => augend, :addend => addend} = inports
    %{:sum => sum} = outports
    receive do
      {:augend, value} when not is_nil(addend) ->
        sum = ElixiFBP.Component.send_ip(sum, addend + value)
        outports = %{outports | :sum => sum}
        inports = %{inports | :augend => nil, :addend => nil}
        loop(inports, outports)
      {:augend, value} ->
        inports = %{inports | :augend => value}
        loop(inports, outports)
      {:addend, value} when not is_nil(augend) ->
        sum = ElixiFBP.Component.send_ip(sum, value + augend)
        outports = %{outports | :sum => sum}
        inports = %{inports | :augend => nil, :addend => nil}
        loop(inports, outports)
      {:addend, value} ->
        inports = %{inports | :addend => value}
        loop(inports, outports)
    end
  end
end
I'll explain what's going on in this new version of Math.Add in the next post.

No comments:

Post a Comment