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:
Objects object oriented programming represents concepts as "objects" that have data fields and associated procedures known as methods. Objects, which are usually instances of classes, are used to interact with one another to design applications and computer programs.
Boxes the box calculus is defined in (G Grov, 2007). Finite state automata are used to structure communicating programs into a series of "boxes", where each box maps inputs to outputs. Boxes are linked by wires and controlled by generalised transitions.
Actors the actor model is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent digital computation. In response to a message that it receives, an actor can make local decisions, create more actors, send more messages, and determine how to respond to the next message received.
Agents agent-oriented programming centres on the concept of software agents, it has externally specified agents at its core. They can be thought of as abstractions of objects. Exchanged messages are interpreted by receiving "agents", in a way specific to its class of agents.
Here are the six implementations under review:
Basic Unit | Realisation | Special Features | |
---|---|---|---|
C++ | (Functional) objects | Language | Multi-paradigm, adds object orientation to C. |
Haskell | Language | GHC, supports [many extensions][ghc-extensions] to Haskell 2010. | |
Hume | Boxes | Language | Targets embedded systems, C and VHDL backends. |
Erlang | Actors | Language | Scalable VM with fault tolerance built in. |
eXAT | Agents | Erlang library | Erlang library implementation of FIPA agent standard. |
Jade | Java library | Java library implementation of FIPA standard. |
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.
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 |
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 |
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); |
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. |
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.
The FIPA standard is an agent oriented programming abstraction:
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. |
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:
Just as eXAT requires definitions of the gen_server
interface
functions, Jade users must adopt Jade conventions. That is, each
agent must be instantiated and assigned behaviours, by overriding
methods of the Agent
and Behaviour
superclasses.
Java is a verbose language. To save space I have omitted the ontology Java code, it totals 125 lines of Java. For a discussion on the verbosity of Jade agents vs eXAT, I would encourage the reader to inspect the language comparison in Figure 3 of (A Stefano, 2009).
The eXAT implementation uses simple String tokenising to parse
instructions such as "increment 4". The Jade framework has support
for defining ontology's with Java objects and Java reflection, to
serialise and parse object-oriented ontology definitions to and from
strings. The Jade calculator and client agents below share a custom
Calculator
ontology. As an example the "increment 4" instruction,
when the getContentManager().fillContent(..)
method is called is:
((action (agent-identifier :name calculator@my.laptop:1099/JADE) (CalculatorOperation :type 1 :value 4)))
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) { } } } |
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:
FIPA Haskell Implementation Implement the FIPA specification and agent framework with the CloudHaskell Platform. To get started, here is the FIPA specification document listing the 22 performatives and their meaning.
RDF As FIPA Message Content Standard The eXAT and Jade examples above demonstrate how to overcome the FIPA content String type restriction. Jade uses Java reflection to serialise and parse ontological information in ACL messages, but is not straight forward to use or debug for developers unfamiliar to reflection. Myself and others have proposed RDF as an alternative solution to encapsulate ontological meaning in Jade messages. RDF is a family of (W3C) specifications is used for conceptual description or modeling of information that is implemented in web resources. This idea could be borrowed in the CloudHaskell FIPA agent system, and the rdf4h Haskell library can be used for parsing and serialising RDF content between agents.
Interoperate with Jade & eXAT via MTP Implement the message transport protocol (MTP) with HTTP using one of the popular Haskell web servers, so that this agent framework can interoperate with Jade and eXAT via MTP.
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).