UP | HOME

Erlang

Once asynchronous message-passing has been integrated into the language runtime as the most fundamental feature (and exposed to the language as just a couple of primitives), communication within a node and across nodes becomes uniform.

Having just that makes scalability and fault tolerance uniform and straightforward.

Another fundamental design decision was that at an implementation level of the runtime there is no sharing of memory, so it is not an emulation of a theoretical property. With JVM they just emulate it.

Erlang (as a project) did everything right. They began with fundamental principles and serious research and found out that Java is fundamentally flawed (and published this finding in the Joe Armstrong’s thesis and in the books).

There is the principle which hardware designers knew all along - one cannot made a truly asynchronous (non-blocking) system with blocking primitives. This is why they have interrupts, queues, ports and buses.

Another fundamental principle is total isolation of processes (to share nothing, not a single byte). In the hardware world this is taken for granted.

The only way to communicate between totally isolated entities is message passing. This principle goes all the way down to cell (and even within-a-cell) communication in biology.

That message passing must be truly asynchronous (they call it “send and pray”) because “blocking” does not work in biology (the principle which Evolution “found out”).

One cannot use just ordinary procedure calls (which use stack), so they invented the spawn asynchronous primitive.

It takes a functional closure, its arguments (everything is, of course, immutable values), and create a new, fully isolated, share-nothing lightweight process with eventually will run that closure and communicate the result back via asynchronous message.

A process is a lightweight virtual machine (which evaluates a byte-compiled code) that can communicate with other processes only by sending and receiving messages.

Erlang’s processes are executed and scheduled by other totally isolated processes, within a supervision hierarchy. Message-delivery subsystem is also isolated and supervised, and it began with the properly designed runtime system which guarantee the share-nothing properties of the lightweight processes.

There is a catch. If your stack does not use these share-nothing, asynchronous primitives (implemented without any sharing), you have an illusion of asynchronity. Processes share no memory, have no shared stack.

Not just that, if your implementation language is an imperative crap with objects, exceptions and multi-threading - you are fucked. These things simply do not combine into a coherent whole.

Pure functions, persistent data structures, isolation of processes and asynchronous message-passing (with append-only mailboxes) together solve the problems, and Erlang designers and researchers got it right.

Since then Clojure and Akka are striving to achieve some similar results, but the problem is that its target platform (the runtime) is not designed for true asynchronity - it only emulates it using blocking and sharing primitives.

Why

Conceptually, just like in a general purpose Functional language, functions from a module are being evaluated by the language’s runtime. The runtime usually does some kind of graph reduction, where the graph is basically a structure made out of nesting of pure functions.

In Erlang one or more function can be evaluated within (inside of) a single totally isolated, lightweight concurrent process (sort of a co-routine) which shares nothing with other similar processes. Thus the runtime partitions the functions into separate processes, just the way we observe in the real world.

This runtime has been explicitly designed, based on the fundamental principles, to ensure that there is nothing shared between the processes and that asynchronous but reliable communication (using persistent, append-only “mailboxes”) can done used structured message-passing (as structured immutable values) instead of calling methods, which share the stack and other contexts.

This is the reason to use the Erlang OTP platform. Despite ancient Prolog-like syntax it otherwise exceptionally well designed and is suitable for functional programming with asynchronous streams, which is what Spark and other huge frameworks do.

The crucial features are:

  • truly asynchronous message-passing instead of calling methods (synchronously)
  • whole values are being passed - no hidden state or pointers to mutable data
  • the send and receive primitives available for pure functions
  • structural pattern-matching on recieve (the killer feature)
  • the spawn primitive which starts a new process (this is how you non-block).
  • “addressing” and the mailboxes are implicitly maintained by the runtime.

The fundamental “pattern” is that once a function receives its data as a message it sends it to a “helper” (delegates) and forgets (sleeps on receive), thus not blocking anything for a long time.

The data and the resulting values are being passed in messages, not just in arguments. Arguments are used to parameterize and specialize functions.

Websockets and framing in general

It seems, at least in theory, the Erlang is the best language for writing advanced client applications - it has the right implementation of “core” protocols and underlying low level primitives, such as processes communicating through asynchronous message passing.

There is, however, no non-bullshit implementation of the websockets protocol, particularly OTP does not support it yet.

It seems that frames and message maps particularly nice to pattern-matching on receive, which is the Erlang’s killer feature, and an these “messages” are an appropriate level of abstraction to work with.

Someone has to write it, but the problem is a niche language (even good one) requires a lot of time to become familiar and requires to read a lot of cryptic code which requires good understanding of the underlying model and abstractions.

No one does such kind of work for free, because it requires months of uninterrupted effort, but food and money has to come from somewhere.

SSL

Erlang OTP team have done a great job for maintaining the ssl library, but now there must be an easy way to let it use /etc/ssl/certs/ or /etc/ssl/certs/java/cacerts

Tools

Use Emacs or Vim. Both have support for TAGS and well as modern tooling.

Do not use distel, it is outdated. Use erlang_ls and erlfmt instead.

The erlang-mode package for Emacs comes bundled with an Erlang/OTP distribution.

Here is a basic configuration. It uses Doom Emacs macros

(add-load-path! (car (file-expand-wildcards "/usr/lib64/erlang/lib/tools-*/emacs")))

(use-package! erlang
  :hook (erlang-mode . lsp)
  :config
  (setq erlang-root-dir "/usr/lib64/erlang")
  (setq lsp-ui-doc-enable t))

And for pre-commit

- repo: local
  hooks:
      - id: erlfmt
        name: erlfmt
        description: Auto-formatter for Erlang code
        entry: erlfmt
        language: system
        files: "\\.erl$"

For Neovim, both nvim-lspconfig and null_ls.nvim know about erlang_ls and erlfmt respectively.

local servers = {
  erlangls = {},
}
local lspconfig = require 'lspconfig'
for s in pairs(servers) do
  local opts = vim.tbl_deep_extend('force', options, servers[s] or {})
  lspconfig[s].setup(opts)
end

for null_ls that would be something like

  local nls = require 'null-ls'
  nls.setup {
    sources = {
      nls.builtins.formatting.erlfmt,
      },
    root_dir = require('null-ls.utils').root_pattern('.null-ls-root', '.nvim.settings.json', '.git'),
  }

There are specialized plugins too:

use { 'vim-erlang/vim-erlang-tags', ft = 'erlang' }
use { 'vim-erlang/vim-erlang-compiler', ft = 'erlang' }
use { 'vim-erlang/vim-erlang-runtime', ft = 'erlang' }
use { 'vim-erlang/vim-erlang-omnicomplete', ft = 'erlang' }

Author: <schiptsov@gmail.com>

Email: lngnmn2@yahoo.com

Created: 2023-08-08 Tue 18:38

Emacs 29.1.50 (Org mode 9.7-pre)