9fans archive / 2004 / 03 / 177 / prev next
From: rog@vit...
Subject: Re: [9fans] Re: advantages of limbo
Date: Tue, 2 Mar 2004 15:04:25 0000
> Yet [Java] is more powerful in expressing algorithms. Things like closures
> or message polymorphism are natural and easy to express in Java, while
> either not possible or inconvenient in limbo.
re: closures: cheap and easy threads provide a more than adequate
substitute in many cases. a channel can represent a connection to a
remote procedure with its attached data.
for instance, a trivial example:
getop(s: string): chan of (string, chan of string)
{
c := chan of (string, chan of string);
spawn append(s, c);
return c;
}
append(s: string, c: chan of (string, chan of string))
{
for(;;){
(t, reply) := <-c;
reply <-= s + t;
}
}
in this example, the channel returned by getop(s)
is functionally equivalent to λt.(s + t).
the generic operator would look like:
op(s: string, c: chan of (string, chan of string)): string
{
reply := chan of string;
c <-= (s, reply);
return <-reply;
}
of course in this case the amount of code compared with its actual
function is considerable, but for less trivial applications this is
often viable (moreover, it's considerably more powerful, as the
channel represents an ongoing continuation rather than just a closure).
re: message polymorphism.
strings are often used for this kind of thing in limbo, as they're
easy to use, by-value (yet locally mutable) and it's trivial to
transfer them externally.
generally, Limbo programs tend to avoid polymorphism (although the
latest version does have parametric polymorphism) in favour of a
simple, direct style of coding.
channels i mentioned earlier in this respect. dynamically loaded
modules mean that you can have several dynamically loaded pieces of
code each complying to the same interface.
i've used this to pleasing effect in some limbo programs. for
example, a piece of "grid" software i did recently allows one to farm
out pieces of work to multiple clients and reliably collect them. the
module that actually splits up the work and marshals the results is
separately implemented. its interface is entirely specified by the
following self-contained module definition:
Taskgenerator: module {
init: fn(jobid: string, state: string, args: list of string): string;
taskcount: fn(): int;
state: fn(): string;
start: fn(id: string,
tries: int,
spec: ref Clientspec,
read: chan of (int, chan of array of byte, chan of int),
write: chan of (array of byte, chan of string, chan of int),
finish: chan of (int, big, chan of string)): (int, string);
reconnect: fn(id: string,
read: chan of (int, chan of array of byte, chan of int),
write: chan of (array of byte, chan of string, chan of int),
finish: chan of (int, big, chan of string)): int;
complete: fn();
quit: fn();
Clientspec: adt {
addr: string;
attrs: list of (string, string);
nodeattrs: list of (string, string);
};
Started, Error, Nomore: con iota;
};
this uses the above mentioned forms of polymorphism in several ways:
multiple different implementations of Taskgenerator modules coexist
concurrently, each with its own unrelated state.
calling start() asks the task generator to start a new task; it
returns some channels that can then be used by the core software to
communicate with an instance of that task (usually a thread will be
started). the interface is straightforward, and both sides of the
software can be written in a straightforward manner.
strings are used to hold arbitrary data (attribute-value pairs,
arbitrary client-created identifiers).
implementation is almost completely divorced from interface, making
the whole thing highly modular. i can replace task generator modules
at run time with no hassle (and often do).
of course, it's no magic bullet, but the features of Limbo do seem to
me to work in synergy to produce a language that spurs creativity
rather than inviting perplexity.
cheers,
rog.