← Code Compare

Maps

Maps (a.k.a. dicts, hashes, associative tables) are the BEAM's go-to key/value store, and the shared task here shows their three core moves: build {"ada" => 36, "grace" => 45}, look up "ada", then return an updated copy with "ada" bumped to 37 without mutating the original. Notice that Erlang, Elixir, LFE, and Gleam are all immutable - "updating" really means producing a fresh map and leaving the old one untouched - while Luerl's Lua tables are genuinely mutable, so it has to copy by hand to mimic the BEAM. Watch too how lookup is modelled: Gleam returns a Result instead of crashing or returning nil, whereas the others lean on the {ok, _}/:error convention or a sentinel.

Show: ErlangElixirGleamLFELuerl
Erlang
-module(ages).
-export([run/0]).

run() ->
    Ages = #{"ada" => 36, "grace" => 45},
    %% Lookup; maps:get/2 crashes if the key is missing (use maps:get/3 for a default).
    AdaAge = maps:get("ada", Ages),
    %% Return an updated *copy*; Ages is unchanged.
    Updated = maps:update("ada", AdaAge + 1, Ages),
    io:format("~p~n", [Updated]),
    Updated.
%% => #{"ada" => 37, "grace" => 45}

Erlang's map literal is #{K => V}; maps:get/2 reads a key and maps:update/3 returns a brand-new map with one key changed, leaving the original Ages binding fully intact.

Elixir
ages = %{"ada" => 36, "grace" => 45}

# Lookup: ages["ada"] returns the value or nil if absent.
ada_age = ages["ada"]

# Immutable update: Map.update/4 builds a fresh map.
updated = Map.update(ages, "ada", 0, fn age -> age + 1 end)

IO.inspect(updated)
# => %{"ada" => 37, "grace" => 45}

Elixir reads a key with the ages["ada"] access syntax and updates with Map.update/4, whose function receives the existing value; ages itself never changes because maps are immutable.

Gleam
import gleam/io
import gleam/int
import gleam/dict

pub fn main() {
  let ages =
    dict.from_list([#("ada", 36), #("grace", 45)])

  // Lookup returns Result(Int, Nil) -- never nil, never a crash.
  let ada_age = case dict.get(ages, "ada") {
    Ok(age) -> age
    Error(Nil) -> 0
  }

  // Immutable insert returns a new dict with "ada" replaced.
  let updated = dict.insert(ages, "ada", ada_age + 1)

  case dict.get(updated, "ada") {
    Ok(age) -> io.println("ada is now " <> int.to_string(age))
    Error(Nil) -> io.println("ada not found")
  }
  // prints: ada is now 37
}

Gleam uses gleam/dict, and dict.get returns a Result(Int, Nil) so a missing key is a value you must handle in a case rather than a nil or an exception; dict.insert returns a fresh dict.

LFE
(defmodule ages
  (export (run 0)))

(defun run ()
  (let* ((ages (map "ada" 36 "grace" 45))
         ;; Lookup the current value.
         (ada-age (maps:get "ada" ages))
         ;; maps:update returns an updated *copy*.
         (updated (maps:update "ada" (+ ada-age 1) ages)))
    (io:format "~p~n" (list updated))
    updated))
;; (ages:run) => #M("ada" 37 "grace" 45)

LFE builds a map with the (map k v ...) form and calls the same Erlang maps:get/maps:update functions; let* threads each step, and the original ages is left unchanged.

Luerl
local ages = { ada = 36, grace = 45 }

-- Lookup is plain table indexing (returns nil if absent).
local ada_age = ages.ada

-- Lua tables are MUTABLE, so to update "immutably" we copy first.
local function with_updated(t, key, value)
  local copy = {}
  for k, v in pairs(t) do copy[k] = v end
  copy[key] = value
  return copy
end

local updated = with_updated(ages, "ada", ada_age + 1)

print(updated.ada, updated.grace)  --> 37  45
print(ages.ada)                    --> 36 (original untouched)

Lua tables are mutable, so the BEAM's immutable update is faked by shallow-copying with a pairs loop before changing the key; lookup is just ages.ada, which yields nil for a missing key.