Skip to content

Design Ideas

Nicolas Trangez edited this page Mar 22, 2023 · 10 revisions

Note: this page was used to write down some initial ideas. Given further experimentation/coding, some of it may no longer be correct/relevant.

General

  • There's no intent to work cross-node. This library allows to build actor-style applications within a single OS process.

  • A process (a.k.a. actor) is a schedulable unit of work (think thread) runs in a ProcessM monad which is layered on top of IO.

  • ProcessM is not a monad transformer, it's the base monad for any process.

  • ProcessM provides instances of the basic classes (Functor, Applicative, Monad, MonadFail, MonadIO,...), as well as various classes for which IO implementations exist, e.g., MonadThrow/MonadCatch/MonadMask, MonadRandom/MonadSplit StdGen and others. To investigate: MonadUnliftIO (if this makes sense), MonadLogger, MonadManaged (from managed), MonadTime (from monad-time), MonadLog (from logging-effect),...

  • The primitive actions needed to implement higher-level functionality are provided by a MonadProcess class which ProcessM implements. Every primitive action gets a default implementation which lifts it through MonadTrans (alternatively, Generically1 could be used, though currently I don't know how). Given that, all transformers (think ReaderT, StateT,...) get MonadProcess instances. Given that, one can easily layer transformers over ProcessM.

    Part of the rationale here is that this allows to implement e.g., the Erlang per-process dictionary functionality by simply layering StateT over ProcessM in some application.

  • Semantics and operations on processes are those of Erlang. Hence, processes can exchange messages, and send/receive signals (think exceptions).

  • Alike Erlang, processes can be linked and monitored.

  • Any cross-process interactions are handled in STM, except for asynchronous exception delivery where applicable. This STM interface is somewhat exposed, such that a ProcessM can, e.g., receiveSTM some message and update some (shared, STM) data within a single transaction.

  • All exception handling happens through the safe-exceptions library. In essence, this should only be used for bracket-style resource handling anyway: in general, processes should die when exceptions occur.

  • It likely makes sense to use async and Asyncs to create and manage the process threads.

  • Messages that can be sent to a process are not type-bound, similar to distributed-process. The implementation can use Dynamic as the type of exchanged messages, unlike the Message (with fingerprint) type found in distributed-process. Other than that, implementation of the mailbox, including match infrastructure, can be very similar. If desired, type-restricted interfaces (e.g., a typed gen_server) can be easily layered on top.

  • Messages are NFData and are fully evaluated within the sender before being added into the target process' mailbox.

  • The core functionality is kept as simple as possible. The library does provide higher-level constructs, like a process registry (not using Strings/atoms as identifiers, but an ADT with constructors specific to the application), behviours like gen_server, supervisor, gen_tcp and others.

    gen_tcp and similar may not make sense, GHC has good support for IO/networking, and there's nothing wrong with using plain sockets in a ProcessM, letting the process die upon socket errors/disconnect/..., whatever seems applicable.

  • Could consider abstracting all concurrency/communication/IO actions using io-classes so the implementation, or higher-level applications, can be tested using io-sim.

  • Unlike Erlang, we could consider having processes with bounded mailboxes, either blocking (i.e., block a sender when the mailbox is full), or lossy non-blocking, either dropping the last message (or throw an exception in the sender), or drop old messages.

  • The system exposes metrics using the OpenMetrics (Prometheus) format. Metrics can include number of actors, mailbox sizes, message delivery latencies,...

  • Actors can have a name, which shows up in metrics.

gen_server

  • A bit like how it's done in Drama, a gen_server implementation could enforce the use of some GADT as the message type which can either be used for casts (type parameter is ()), or call (type parameter can be anything, and specifies the return type of the call).
  • Have an easy way to expose a gen_server over the network, using CBOR serialization (the serialise package) for the messages, as well as a client. Given a message type m resp, this can be casted (iff resp is ()) and called over a TCP socket to the server using a JSON-RPC-like protocol.
Clone this wiki locally