Friday, June 16, 2023

Egel Distributed Programming

Egel is based on graph rewriting semantics, at any point the program forms a directed acyclic graph of combinators and evaluation is achieved by trampolining the root node. The Egel language is a bare-bones front-end to that semantics, roughly comparable to a lambda calculus with constants where constants compose.

Because of this semantics, a number of things are more 'easy' to implement than in more traditional languages. Egel is implemented in C++ without an explicit garbage collector, parallel evaluation is simply spawning of another root node, and the graph -or parts of the graph- can be serialized, shipped, or saved to disc.

Distributed programming involves picking up parts of the graph and shipping those to a remote node for evaluation. There are various manners in which the model allows for a distributed implementation but I went for a relatively straightforward solution, given the runtime I already had.

Servers, and clients, are implemented atop of Google's Protobuf. Preceding every call, the client does a best-effort scan of the graph and sends a bundle of referenced combinators, the code, to the server. After that, the term is sent and evaluated on the server which will send a term back. Referencing combinators that are not present for whatever reason (for instance, opaque objects like file handles or dynamically loaded c code) is undefined behaviour.

Now for some code, the following defines a server, it starts a service and then blocks waiting for remote calls.

import "egel_rpc.ego"

using System

def main = rpc_server "localhost:50001"

Given such a server, a client can ask for terms to be evaluated. Because the Egel language doesn't have process constructs, we capture what is to be evaluated remotely within a lambda.

import "egel_rpc.ego"

using System

def main = 
    let C = rpc_client "localhost:50001" in
    rpc_call C [_ -> [X -> X] ] 42

In the above example, a lambda is sent to server, that returns the identity function to the client, and the client applies that to the constant 42.

Another example, we generate a list of values on the server, and sum that list on the client.

import "prelude.eg"
import "egel_rpc.ego"

using System
using List

def main = 
    let C = rpc_client "localhost:50001" in
    rpc_call C [_ -> from_to 1 1000 ] |> sum
It works reasonably well but I am still wrinkling out the features.

Note: the Egel interpreter is a hobby project and in beta. It is roughly useable but slow and everything is still subject to change.