JSON
One round-trip across all five BEAM languages: parse {"name":"ada","age":36}, bump age to 37, and serialise it back to a JSON string. Watch who ships JSON in the box versus who reaches for a library - modern Erlang/OTP 27+ has a built-in json module and Elixir leans on the ubiquitous Jason (or its own JSON since 1.18), Gleam uses gleam_json with a typed decoder, LFE just calls Erlang's json, and Luerl's plain Lua has no JSON library at all, so the idiom is to hand-roll a tiny encoder. Notice the recurring BEAM theme: object keys decode to binaries (Erlang/Elixir/LFE), Gleam makes you declare the shape up front and returns a Result, and only Lua mutates the table in place.
-module(bump).
-export([run/0]).
run() ->
Input = <<"{\"name\":\"ada\",\"age\":36}">>,
%% OTP 27+ ships a built-in json module; keys decode to binaries.
Decoded = json:decode(Input),
#{<<"age">> := Age} = Decoded,
Updated = Decoded#{<<"age">> => Age + 1},
%% json:encode/1 returns an iolist; flatten it to a binary.
Out = iolist_to_binary(json:encode(Updated)),
io:format("~s~n", [Out]),
Out.
%% => {"name":"ada","age":37}Since OTP 27 Erlang has a built-in json module: json:decode/1 turns the binary into a map with binary keys, the #{... => ...} syntax returns an updated copy, and json:encode/1 produces an iolist you flatten with iolist_to_binary/1. Pre-27 code reaches for jsx instead.
input = ~s({"name":"ada","age":36})
# Jason is the de-facto JSON library (Elixir 1.18+ also has a built-in JSON).
{:ok, data} = Jason.decode(input)
updated = Map.update!(data, "age", &(&1 + 1))
updated
|> Jason.encode!()
|> IO.puts()
# => {"name":"ada","age":37}Jason.decode/1 returns a tagged {:ok, map} with string keys, Map.update!/3 bumps age with the &(&1 + 1) capture, and the result pipes through Jason.encode!/1 (the ! variant raises instead of returning a tuple) to IO.puts.
import gleam/io
import gleam/json
import gleam/dynamic/decode
pub type Person {
Person(name: String, age: Int)
}
fn person_decoder() -> decode.Decoder(Person) {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
decode.success(Person(name:, age:))
}
pub fn main() {
let input = "{\"name\":\"ada\",\"age\":36}"
let assert Ok(person) =
json.parse(from: input, using: person_decoder())
let updated = Person(..person, age: person.age + 1)
let out =
json.object([
#("name", json.string(updated.name)),
#("age", json.int(updated.age)),
])
|> json.to_string
io.println(out)
// => {"name":"ada","age":37}
}Gleam makes the shape explicit: a decode.Decoder declares each field's type, json.parse returns a Result you must handle (here let assert Ok), and you rebuild the JSON from json.object/json.string/json.int - there is no untyped map and no null.
(defmodule bump
(export (run 0)))
(defun run ()
(let* ((input #"{\"name\":\"ada\",\"age\":36}")
;; Same OTP 27+ json module as Erlang; keys are binaries.
(decoded (json:decode input))
(age (maps:get #"age" decoded))
(updated (maps:put #"age" (+ age 1) decoded))
(out (iolist_to_binary (json:encode updated))))
(io:format "~s~n" (list out))
out))
;; (bump:run) => {"name":"ada","age":37}LFE shares Erlang's runtime, so it calls the identical json:decode/json:encode; #"..." is the binary literal, maps:get/maps:put read and update the decoded map (binary keys again), and let* threads each step in Lisp form.
-- Standard Lua (what Luerl runs) ships NO json library, so we hand-roll.
local function encode(t)
return string.format('{"name":%q,"age":%d}', t.name, t.age)
end
-- For this fixed shape, "parsing" is a literal table; real code would
-- use a json.lua module. Lua tables are MUTABLE, so we edit in place.
local person = { name = "ada", age = 36 }
person.age = person.age + 1
print(encode(person))
-- => {"name":"ada","age":37}Plain Lua has no JSON in its standard library, so the idiom is a tiny hand-rolled encoder (string.format with %q for a quoted string and %d for the number); unlike the immutable BEAM languages, the Lua table is mutated in place with person.age = person.age + 1.