First Impressions of Erlang: A Go Engineer’s Perspective

Leonardo
7 min readJun 19, 2024

As a software engineer with significant experience in Golang, getting into Erlang has been both intriguing and enlightening. For those unfamiliar, Erlang is a programming language designed for building robust, concurrent, and distributed systems. My journey into Erlang began with Joe Armstrong’s book, “Programming Erlang: Software for a Concurrent World,” which is an authoritative resource on the language and its applications. This exploration gave me a fresh perspective on the paradigms that both languages embrace and allowed me to compare how similar problems are tackled in each environment.

The Basics: Syntax and Structure

Erlang’s syntax is a departure from the more familiar C-like syntax of Golang. It is reminiscent of Prolog, with its roots in the functional programming paradigm. This means that Erlang emphasizes immutable data, recursive functions, and pattern matching, which are quite different from Go’s procedural and straightforward approach.

Here’s a basic example of a client and server interaction in Erlang.

Client Module

-module(afile_client).

-export([ls/1, get_file/2]).

ls(Server) ->
Server ! {self(), list_dir},
receive
{Server, FileList} ->
FileList
end.

get_file(Server, File) ->
Server ! {self(), {get_file, File}},
receive
{Server, Content} ->
Content
end.

Let’s break this program step-by-step:

1. -module(afile_client).
— This line declares the name of the module, which in this case is afile_client. In Erlang, each file generally corresponds to a module, and the module name should match the file name.

2. -export([ls/1, get_file/2]).
— This line specifies which functions are publicly accessible from outside the module. Here, ls/1 and get_file/2 are being exported.
ls/1 means there is a function ls with one argument.
get_file/2 means there is a function get_file with two arguments.

3. ls(Server) ->
— This defines the function ls that takes one parameter, Server. This parameter is expected to be a process identifier (pid) of the server process with which this client communicates.

4. Server ! {self(), list_dir},
— This line sends a message to the Server. The message is a tuple containing two elements: self() and list_dir.
self() is a built-in function that returns the process ID of the current process (in this case, the client).
list_dir is a command that the server understands, instructing it to list the directory contents.

5. receive
— This keyword starts a block that allows the process to wait for a message that matches a specified pattern.

6. {Server, FileList} ->
— This pattern matches messages that are tuples where the first element is the Server’s pid, and the second element is the FileList. It assumes that the server responds with a tuple containing itself as the first element and the directory file list as the second.

7. FileList
— This is the return value of the ls function. If the received message matches the specified pattern, the function returns FileList, which contains the directory listing from the server.

8. end.
— This keyword ends the receive block.

9. get_file(Server, File) ->
— This defines another function get_file that takes two parameters: Server (the server’s pid) and File (the name of the file to retrieve).

10. Server ! {self(), {get_file, File}},
— Similar to the ls function, this line sends a message to the Server. The message is a tuple containing the client’s pid and another tuple with the command get_file and the file name File.

11. receive
— Starts another receive block to wait for the server’s response.

12. {Server, Content} ->
— This pattern matches messages that are tuples where the first element is the Server’s pid, and the second element is Content of the requested file.

13. Content
— This is the return value of the get_file function. If the received message matches the specified pattern, the function returns the content of the file requested.

14. end.
— Ends the receive block and also the definition of the get_file function.

Each of these lines is crucial for enabling basic client-server file operations in an Erlang application where processes communicate asynchronously via message passing.

Server Module

-module(afile_server).

-export([start/1, loop/1]).

start(Dir) ->
spawn(afile_server, loop, [Dir]).

loop(Dir) ->
receive
{Client, list_dir} ->
Client ! {self(), file:list_dir(Dir)};
{Client, {get_file, File}} ->
Full = filename:join(Dir, File),
Client ! {self(), file:read_file(Full)}
end,
loop(Dir).

Let’s break down the Erlang server code step by step:

1. -module(afile_server).
— This line declares the module name as `afile_server`. This is the name of the module that the code defines.

2. -export([start/1, loop/1]).
— This line specifies which functions are available to external modules. start/1 is a function that takes one argument, and loop/1 is a function that takes one argument.

3. start(Dir) ->
— This function, start, takes one argument Dir, which is the directory path. It is responsible for starting the server.

4. spawn(afile_server, loop, [Dir]).
— This line uses the spawn function to create a new process.
afile_server is the module name.
loop is the function to be executed in the new process.
[Dir] is the argument passed to the loop function, which is the directory path.

5. loop(Dir) ->
— This defines the loop function that takes one argument Dir. This function will handle incoming messages in a loop.

6. receive
— This starts a receive block that waits for messages sent to this process.

7. {Client, list_dir} ->
— This pattern matches a message {Client, list_dir}.
Client is the process that sent the message.
list_dir is the command indicating that the client wants to list the directory contents.

8. Client ! {self(), file:list_dir(Dir)};
— This sends a message back to Client.
{self(), file:list_dir(Dir)} is the message, where self() is the process ID of the current process.
file:list_dir(Dir) is a call to Erlang’s file module to list the contents of the directory Dir.

9. {Client, {get_file, File}} ->
— This pattern matches a message {Client, {get_file, File}}.
Client is the process that sent the message.
{get_file, File} is the command indicating that the client wants to get the contents of the file File.

10. Full = filename:join(Dir, File),
— This line constructs the full file path by joining the directory Dir and the file name File using filename:join/2.

11. Client ! {self(), file:read_file(Full)}
— This sends a message back to Client.
{self(), file:read_file(Full)} is the message, where self() is the process ID of the current process.
file:read_file(Full) reads the contents of the file located at Full.

12. end,
— This ends the receive block.

13. loop(Dir).
— This recursively calls loop/1 with the same directory Dir, creating an infinite loop to handle subsequent messages.

Putting it all together, the afile_server module starts a server process with the specified directory and enters a loop, waiting to receive messages from clients requesting directory listings or file contents. Each request is handled by sending a message back to the client with the appropriate data.

Concurrency and Fault Tolerance

One of Erlang’s hallmarks is its built-in support for lightweight processes and message passing. Unlike Go’s goroutines, Erlang processes do not share any memory and communicate exclusively through message passing. This model provides strong fault isolation between processes. Additionally, Erlang’s runtime system enables systems to be distributed over multiple nodes with little to no change in code.

In Go, concurrency is handled through goroutines and channels, which provide a powerful yet different approach for the same problem with similar functionality:

package main

import (
"os"
"path/filepath"
)

type Command int

const (
ListDir Command = iota
GetFile
)

type Request struct {
Command Command
Args []string
Client chan Response
}

type Response struct {
Data interface{}
}

func start(dir string) chan Request {
requests := make(chan Request)
go loop(dir, requests)
return requests
}

func loop(dir string, requests chan Request) {
for {
request := <-requests
switch request.Command {
case ListDir:
files, err := os.ReadDir(dir)
if err != nil {
request.Client <- Response{Data: err.Error()}
continue
}
fileNames := make([]string, len(files))
for i, file := range files {
fileNames[i] = file.Name()
}
request.Client <- Response{Data: fileNames}
case GetFile:
if len(request.Args) < 1 {
request.Client <- Response{Data: "Filename not specified"}
continue
}
fullPath := filepath.Join(dir, request.Args[0])
data, err := os.ReadFile(fullPath)
if err != nil {
request.Client <- Response{Data: err.Error()}
continue
}
request.Client <- Response{Data: data}
}
}
}

func main() {
// Example usage
dir := "./" // the directory to manage
requests := start(dir) // Start the file server and get the requests channel

clientChan := make(chan Response)
requests <- Request{Command: ListDir, Client: clientChan} // Send list directory request
response := <-clientChan // Receive the response
println("Directory listing:")
if fileNames, ok := response.Data.([]string); ok {
for _, name := range fileNames {
println(name)
}
} else {
println("Error:", response.Data)
}

// Send get file request
requests <- Request{Command: GetFile, Args: []string{"main.go"}, Client: clientChan}
response = <-clientChan // Receive the response
if data, ok := response.Data.([]byte); ok {
println("File contents:", string(data))
} else {
println("Error:", response.Data)
}
}

Performance Considerations

While Erlang excels in handling numerous simultaneous connections and maintaining high availability, Go generally offers better performance in terms of raw computational speed due to its static typing and compilation to native code. This makes Go a preferred choice for applications that benefit from high performance and efficient resource utilization.

Conclusion

Exploring Erlang has been a refreshing experience that challenged many of the preconceptions I had as a Go developer. The built-in concurrency model, fault tolerance, and the functional programming paradigm offer a powerful toolkit for certain classes of problems, particularly those involving high concurrency and uptime requirements.

Despite the stark differences in design and philosophy, both Go and Erlang have their merits and ideal use cases. For developers entrenched in the Go ecosystem, Erlang offers insightful lessons in building resilient systems that can gracefully handle failures and load. Whether you’re managing state in distributed systems or simply curious about functional programming, Erlang is worth exploring.

--

--

Leonardo

Software developer, former civil engineer. Musician. Free thinker. Writer.