Programming Shootout - Objects vs Boxes vs Actors vs Agents

Posted on February 3, 2014

Four Programming Paradigms

This post uses a simple calculator specification to demonstrate four programming models. This is a follow-up to my previous post on the agent oriented programming. TL;DR: agent oriented programming is an interesting idea, but does impose burden on the programmer. Let's take agents, with Wikipedia definitions of three other models, to compare:

Six Implementations

Here are the six implementations under review:

Basic UnitRealisationSpecial Features
C++(Functional) objectsLanguageMulti-paradigm, adds object orientation to C.
HaskellLanguageGHC, supports [many extensions][ghc-extensions] to Haskell 2010.
HumeBoxesLanguageTargets embedded systems, C and VHDL backends.
ErlangActorsLanguageScalable VM with fault tolerance built in.
eXATAgentsErlang libraryErlang library implementation of FIPA agent standard.
JadeJava libraryJava library implementation of FIPA standard.


The Calculator Specification

calculator : {inc:Nat → Unit, dec:Nat → Unit, get:Unit → Nat}
calculator = let x = 1 in
               {inc = λy:Nat.  x:=x+y,
                dec = λy:Nat.  x:=x-y,
                get = λ_:Unit. !x     };

Each implementation is split in to two basic units. One is a calculator, with a simple API of inc, dec and get. The other is a client that uses the API to modify the internal state of the calculator. The API is defined as follows:

The calculator's internal state is an integer with the initial value of 0. The client makes four sequential calls to the calculator's API: inc 4 inc 2 dec 1 and get. The client receives a token containing the value 5, reflecting the final internal state of the calculator.


The Shootout...

Objects in C++

C++ is a general purpose statically typed and multi-paradigm programming language that adds object orientation to C, and classes are the central feature of C++. The example below defines a Calculator class, which is instantiated and its internal state modified within the Client program.

Calculator.h
class Box {
 private:
  int x;
 public:
  Box();
  void inc(int y);
  void dec(int y);
  int get();
};
Calculator.cpp
#include "Calculator.h"

Calculator::Calculator ()      { x = 0;    }
void Calculator::inc   (int y) { x += y;   }
void Calculator::dec   (int y) { x -= y;   }
int  Calculator::get   ()      { return x; }
Client.cpp
#include "Calculator.h"
#include 
using namespace std;

int main () {
  Calculator calc;
  calc.inc(4);
  calc.inc(2);
  calc.dec(1);
  printf("Answer: %d\n",calc.get());
  return 0;
}







Functional Objects in Haskell

Haskell is a general purpose non-strict purely functional programming language. The Haskell version of the calculator has been implemented deliberately with mutable functional objects using an MVar, to mimic the object model of the C++ example above. The Calculator data type nicely reflects the types expressed in the calculator specification.

Calculator.hs
module Calculator (Calculator(..),newCalculator) where
import Control.Concurrent.MVar

-- | The calculator API
data Calculator =
        Calculator { inc :: Int -> IO ()
                   , dec :: Int -> IO ()
                   , get :: IO Int }

newCalculator :: IO Calculator
newCalculator = do
  mv <- newMVar 0
  return Calculator
    { inc = \y -> modifyMVar_ mv (\x -> return (x+y))
    , dec = \y -> modifyMVar_ mv (\x -> return (x-y))
    , get = readMVar mv }
Client.hs
module Client where

import Calculator

main :: IO ()
main = do
  calculator <- newCalculator
  inc calculator 4
  inc calculator 2
  dec calculator 1
  get calculator >>= \x ->
   print $ "Answer: " ++ show x





Boxes in Hume

Hume is a novel strongly-typed functionally inspired programming language developed at Heriot-Watt and St. Andrews Universities, intended for resource bounded domains to target real-time embedded platforms and safety critical systems. It is based on concurrent finite state automata controlled by pattern matching and recursive functions over rich types. The language toolkit includes a Hume interpreter and Hume Abstract Machine interpreter, a compiler with a C backend, and recently a VHDL backend to target FPGAs. Below is the Hume implementation of the client and calculator actors using boxes, courtesy of Gudmund Grov:

calculator.hume
data Cmd = Inc | Dec | Get;

box calculator
  in  (cmd :: Cmd,inp :: Int, state :: Int)
  out (outp :: Int,state' :: Int)
match
  (Inc,n,s) -> (*,n+s)
| (Dec,n,s) -> (*,s-n)
| (Get,*,s) -> (s,s);

wire calculator
      (client.cmd,client.arg,calculator.state'
         initially 0)
      (output,calculator.state);
client.hume:
stream output to "std_out";
box client
  in  (state :: Int)
  out (cmd :: Cmd, arg :: Int, state' :: Int)
match
  0 -> (Inc,4,1)
| 1 -> (Inc,2,2)
| 2 -> (Dec,1,3)
| 3 -> (Get,*,*);

wire client
      (client.state' initially 0)
      (calculator.cmd,calculator.inp,client.state);

Actors in Erlang

Erlang is a general-purpose strict concurrent functional programming language and industry quality virtual machine. This implementation uses plain Erlang actors, and is simpler and shorter than the Erlang eXAT agent implementation later. The message passing calculator and client actors most closely honours the message sequence chart describing the specification:

box.erl
-module(box).
-export([start/0,calculator/1]).

start() -> calculator(0).

calculator(I) ->
    receive
        {inc,J} -> calculator(I+J);
        {dec,J} -> calculator(I-J);
        {get,Pid} -> Pid ! I
    end.


client.erl
-module(client).
-export([client/0]).

client() ->
    CalculatorPid = spawn(fun() -> box:start() end),
    CalculatorPid ! {inc,4},
    CalculatorPid ! {inc,2},
    CalculatorPid ! {dec,1},
    CalculatorPid ! {get,self()},
    receive
        I -> io:format("Answer: ~p~n",[I])
    end.

Agents in eXAT

eXAT (A Stefano, 2009) is a FIPA-compliant agent programming platform to write and execute agents in Erlang OTP. Performatives INFORM and REQUEST are used to add context to messages, and the calculator examines the performative of each message it receives before evaluating the message content. The four messages in the implementation are: {INFORM, "inc,4"}, {INFORM, "inc,2"}, {INFORM, "dec,1"}, and {REQUEST, "get"}. eXAT also includes an Erlang based rule-processing engine, an agent execution environment for agent tasks based on finite-state machines, and support for transmitting and handling ACL messages. The source is on GitHub.

What is FIPA?

The FIPA standard is an agent oriented programming abstraction:

FIPA.content: a thorn in the agent side

The FIPA ACL specification states that content of agent message is type restricted to String. In the Erlang implementation, sending messages to the calculator was straight forward, e.g. by sending {inc,2} to ask that it increments the calculator state by 2. The mandatory String type for FIPA messages rules out this solution. There are numerous approaches for dealing with this. The simplest is to use String tokenisation, e.g. to delimit a message "inc,2" from the client agent in to "inc" and "2", which is how the eXAT implementation below overcomes this limitation.

calculator.erl
-module(calculator).
-behaviour(agent).

-export([start/0, stop/0]).
-export([code_change/3, handle_call/3,handle_acl/2,
        handle_cast/2, handle_info/2, init/2, terminate/2]).

-include("acl.hrl").
-include("fipa_ontology.hrl").

%%API
start() -> agent:start_link(calculator, ?MODULE, []).
stop()  -> agent:stop(calculator).

%%agents callback
handle_acl(#aclmessage{speechact='REQUEST',
           sender=Sender}=Msg,State) ->
    spawn(fun () -> acl:reply(Msg, 'INFORM', State) end),
    {noreply, State};

handle_acl(#aclmessage{speechact = 'INFORM',
           sender=Sender}=Msg,State) ->
    Instruction = string:tokens(#aclmessage.content,","),
    Type = lists:nth(1,Instruction),
    Val  = lists:nth(2,Instruction),
    case Type of
        "inc" -> NewState = State + Val;
        "dec" -> NewState = State - Val
    end,
    {noreply, NewState}.

%% gen_server callbacks
init(Name, _Params) -> {ok, Name}.
handle_call(Call, _From, State) ->
  {reply, {error, unknown_call}, State}.
handle_cast(_Call, State) -> {noreply, State}.
handle_info(Msg, State) -> {noreply, State}.
code_change(_, State, _) -> {ok, State}.
terminate(_, _) -> ok.











client.erl
-module(client).
-behaviour(agent).

-export([start/0, stop/0]).
-export([code_change/3, handle_call/3,handle_acl/2,
         handle_cast/2, handle_info/2, init/2, terminate/2]).

-include("acl.hrl").
-include("fipa_ontology.hrl").

%%API
start() -> agent:start_link(clientagent, ?MODULE,
                     [{"localhost", 7778, <<"calculator">>}]).
stop() -> agent:stop(clientagent).

%%agents callback
handle_acl(#aclmessage{speechact = 'INFORM'}=Msg,
           {_,DestAgent}=State) ->
    io:format("Answer: ~s~n", [#aclmessage.content]),
    {noreply, State}.

%% gen_server callbacks
handle_info(ping, {SelfName, DestAgent} = State) ->
    {Ip, Port, Name} = DestAgent,
    Addr = list_to_binary(lists:flatten(
              io_lib:format("http://~s:~b",[Ip, Port]))),
    Dest = #'agent-identifier'{name = Name,addresses = [Addr]},

    PingMsg = #aclmessage{sender = SelfName,
                          receiver = Dest, content = <<"inc,4">>},
    spawn(fun () -> Resp = acl:inform(PingMsg) end),

    PingMsg = #aclmessage{sender = SelfName,
                          receiver = Dest, content = <<"inc,2">>},
    spawn(fun () -> Resp = acl:inform(PingMsg) end),

    PingMsg = #aclmessage{sender = SelfName,
                          receiver = Dest, content = <<"dec,1">>},
    spawn(fun () -> Resp = acl:inform(PingMsg) end),

    {noreply, State};

init(Name, [DestAgent]) -> {ok, {Name, DestAgent}}.
handle_call(Call, _From, State) -> {reply, {error, unknown_call}, State}.
handle_cast(_Call, State) -> {noreply, State}.
handle_info(Msg, State) -> {noreply, State}.
code_change(_, State, _) -> {ok, State}.
terminate(_, _) -> ok.

Agents in Jade

The Jade agent framework (F Bellifemine et al, 1999) is another implementation of the FIPA specification. this time in Java. It is a port of the eXAT code. The reason for the increased code size is threefold:

Calculator.java
package com.box.calculator;

import com.box.calculator.ops.*;
import static com.box.calculator.ops.CalculatorOperation.*;
import jade.content.*;
import jade.content.lang.Codec;
import jade.content.lang.sl.SLCodec;
import jade.content.onto.*;
import jade.content.onto.basic.Action;
import jade.core.Agent;
import jade.core.behaviours.CyclicBehaviour;
import jade.lang.acl.ACLMessage;
import static jade.lang.acl.ACLMessage.*;
import jade.lang.acl.MessageTemplate;

public class Calculator extends Agent implements CalculatorVocabulary {

  Integer x;
  private final Ontology ontology = CalculatorOntology.getInstance();
  private final Codec codec;

  public Calculator() {
    this.codec = new SLCodec();
  }

  @Override
  protected void setup() {
    x = 0;
    getContentManager().registerLanguage(codec);
    getContentManager().registerOntology(ontology);
    addBehaviour(new CalculatorBehaviour());
  }

  class CalculatorBehaviour extends CyclicBehaviour {

    @Override
    public void action() {
      MessageTemplate mt;
      mt = MessageTemplate.MatchOntology(ontology.getName());
      ACLMessage msg = blockingReceive(mt);
      try {
        ContentElement content;
        content = getContentManager().extractContent(msg);
        Concept action = ((Action) content).getAction();
        CalculatorOperation op = (CalculatorOperation) action;
        int performative = msg.getPerformative();
        if (performative == INFORM) {
          switch (op.getType()) {
          case INC: x += op.getValue(); break;
          case DEC: x -= op.getValue(); break;
          default:
            System.out.println("Unexpected calculator operation");
          }
        }
        else if (performative == REQUEST){
          switch (op.getType()){
          case GET:
            ACLMessage reply = msg.createReply();
            reply.setPerformative(INFORM);
            reply.setContent(x.toString());
            send(reply);
            break;
          default:
            System.out.println("Unexpected calculator operation");
          }
        }
      } catch (Codec.CodecException e) { }
        catch (OntologyException e) { }
} } }
Client.java
package com.box.calculator;

import com.box.calculator.ops.*;
import static com.box.calculator.ops.CalculatorOperation.*;
import jade.content.AgentAction;
import jade.content.lang.Codec;
import jade.content.lang.sl.SLCodec;
import jade.content.onto.*;
import jade.content.onto.basic.Action;
import jade.core.*;
import jade.core.behaviours.OneShotBehaviour;
import jade.lang.acl.ACLMessage;

public class Client extends Agent {

  private final AID server = new AID("box", AID.ISLOCALNAME);
  private final Ontology ontology = CalculatorOntology.getInstance();
  private final Codec codec = new SLCodec();

  @Override
  protected void setup() {
    getContentManager().registerLanguage(codec);
    getContentManager().registerOntology(ontology);
    addBehaviour(new ClientBehaviour());
  }

  class ClientBehaviour extends OneShotBehaviour {

    @Override
    public void action() {
      CalculatorOperation co = new CalculatorOperation();
      co.setType(INC);
      co.setValue(4);
      sendMessage(ACLMessage.INFORM, co);

      co.setType(INC);
      co.setValue(2);
      sendMessage(ACLMessage.INFORM, co);

      co.setType(DEC);
      co.setValue(1);
      sendMessage(ACLMessage.INFORM, co);

      co.setType(GET);
      sendMessage(ACLMessage.REQUEST, co);

      ACLMessage reply = myAgent.blockingReceive();
      System.out.println("Answer: " + reply.getContent());
    }
  }

  void sendMessage(int performative, AgentAction action) {
    ACLMessage msg = new ACLMessage(performative);
    msg.setLanguage(codec.getName());
    msg.setOntology(ontology.getName());
    try {
      getContentManager().fillContent(msg,
                                      new Action(server, action));
      msg.addReceiver(server);
      send(msg);
    } catch (Codec.CodecException ex) { }
      catch (OntologyException ex) { }
} }








Project idea: A FIPA Multi-Agent System in CloudHaskell

CloudHaskell is a domain specific Erlang language embedded in Haskell, i.e. Erlang + monads + strong static typing. The original design is described in the Haskell in the Cloud paper, and is on hackage. A CloudHaskell Platform is being developed, which implements Erlang OTP abstractions on top of the CloudHaskell primitives.


The proposal is:

Summary

This post compares four programming models: objects, boxes, actors and agents, by implementing a simple specification with a calculator unit and a client unit. Objected oriented programming with C++ and functional objects in Haskell are the simplest and shortest implementations. The Hume implementation defines the calculator and the client as finite state machines wired together, and again the code size is small. Actor oriented programming in Erlang is the simplest and shortest implementation with explicit message passing. Agent oriented programming is demonstrated twice, with the eXAT Erlang library and the Jade Java library. Both use FIPA performatives INFORM and REQUEST to help receiving agents predict the nature of the message. eXAT uses simple string tokenisation to serialise and parse calculation instructions. Jade uses a custom calculator ontology to structure message content passed to the calculator agent using Java reflection. But be warned:

Agent projects are often initialised with no clear goals in mind. With no goals, there are also no criteria for assessing the success or otherwise of the initiative, and no way of telling whether the project is going well or badly. The net result is that catastrophic project failure can occur seemingly out of the blue (M Wooldridge, 1998).

Without doubt, agent oriented modelling adds overhead to the programming effort. Programs are very often implementable with just plain Erlang actors, Java objects or Haskell etc. For those times where the FIPA standard is a good match, I propose the development of a FIPA platform on top of CloudHaskell Platform that uses the RDF W3C standard for message content, and includes an MTP HTTP component for interoperability with other FIPA platforms including eXAT and Jade. As a motivator:

Autonomous agents and multi-agent systems represent a new way of analysing, designing, and implementing complex software systems. The agent-based view offers a powerful repertoire of tools, techniques, and metaphors that have the potential to considerably improve the way in which people conceptualise and implement many types of software (N Jennings, 1998).