A Useless Machine does one thing: when you turn it on, a mechanical paw reaches out and flips the switch back off. Useless, but fun.
Let's build one in Elixir!
To save us some code, and to give our Useless Machine object permanence, we'll define it as a reactive durable workflow using Journey.
1. Prerequisites
To play with this, you'll want to have Elixir 1.18 or 1.19, Docker (if you want to run your postgres database in a docker container), and some familiarity with Elixir.
If you use asdf, here is the .tool-versions file I used for this exercise:
$ cat .tool-versions
erlang 27.1.2
elixir 1.19.1-otp-27
2. Source Code
The source code for this exercise (+ tests) is available at https://github.com/markmark206/useless_machine
If you want to just start playing with Useless Machine, not implement it, just clone the repo, follow its setup steps, and fast forward to section 5. Useless Machine: Let's Play.
If you want to follow along... well, follow along.
3. Create and Configure the Project
We'll start by creating a basic Elixir project, wiring up its dependencies (postgres, journey), configuring its database and journey logging, fetching dependencies, and creating the database.
This is largely standard Elixir stuff, so I'll list the steps, but won't spend much time discussing them:
Create the initial Elixir project:
$ mix new useless_machine
$ cd useless_machine
Wire Up Dependencies
Update your new project's mix.exs to add Journey, Ecto, and Postgrex:
defp deps do
[
{:journey, "~> 0.10.40"},
{:ecto, "~> 3.12 or ~> 3.13"},
{:postgrex, "~> 0.20 or ~> 0.21"}
]
end
Configure the Database and Logging
Create config/config.exs, with the following configuration:
import Config
config :useless_machine, ecto_repos: [Journey.Repo]
config :journey, Journey.Repo,
database: "useless_machine",
username: "postgres",
password: "postgres",
hostname: "localhost"
config :logger,
:console,
format: "$time [$level] $metadata$message\n",
level: :warning,
metadata: [:pid, :mfa]
Fetch Dependencies
$ mix deps.get
Start a Postgres Instance, Create the Database
Start a Postgres instance in a container:
$ docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:16
Create the database:
$ mix ecto.create
4. Implement the Useless Machine
Now that we have the placeholder project in place, let's implement our Useless Machine.
A Useless Machine may look similar to this:
βββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ββββββββββββββ ββββββββββββββ β
β β β 2. triggers β β β
β β :switch ββββββββββββββΆβ :paw β β
β β (input) β β (mutate) β β
β β βββββββββββββββ β β
β ββββββββββββββ 3. "off" ββββββββββββββ β
β β² β
ββββββββββΌβββββββββββββββββββββββββββββββββββββ
β 1. "on"
βββββ΄ββββ
β user β
βββββββββ
Luckily, it will only be a few lines of code!
Replace the contents of lib/useless_machine.ex with these 15 lines:
defmodule UselessMachine do
import Journey.Node
def graph() do
Journey.new_graph([
input(:switch),
mutate(:paw, [:switch], &lol_no/1, mutates: :switch)
])
end
defp lol_no(%{switch: switch} = _values) do
IO.puts("paw says: '#{switch}? lol no'")
{:ok, "off"}
end
end
This module defines
-
a graph with two nodes:
-
input(:switch)β an input node where we can set a value -
mutate(:paw, [:switch], &lol_no/1, mutates: :switch)β watches:switch, and whenever it changes, runslol_no/1, which sets:switchback to"off"
-
the function,
lol_no/1, which:pawwill execute whenever the:switchis turned "on"
This defines your Useless Machine!
One last thing we need to do is to register the graph with Journey by adding these lines to config/config.exs:
config :journey, :graphs, [
&UselessMachine.graph/0
]
5. Useless Machine: Let's Play
Now that we have everything in place, let's play with our Useless Machine!
Let's fire up iex!
$ iex -S mix
In iex, let's create a new Useless Machine execution:
iex(1)> graph = UselessMachine.graph(); :ok
:ok
iex(2)> execution = graph |> Journey.start(); :ok
:ok
Turn the :switch on!! ... and watch :paw promptly turn the :switch off:
iex(3)> execution = Journey.set(execution, :switch, "on"); :ok
paw says: 'on? lol no'
:ok
iex(4)> Journey.values(execution)
%{
switch: "off",
paw: "updated :switch",
execution_id: "EXECRDYHBJ8X0RVD8BG0A7A5",
last_updated_at: 1764109938
}
Was it a fluke? Let's try again:
iex(5)> execution = Journey.set(execution, :switch, "on"); :ok
paw says: 'on? lol no'
:ok
iex(6)> Journey.values(execution)
%{
switch: "off",
paw: "updated :switch",
execution_id: "EXECRDYHBJ8X0RVD8BG0A7A5",
last_updated_at: 1764109952
}
Ok, not a fluke.
No matter how many times you set the switch to "on", the paw immediately turns it back off.
6. Durable Useless Machine
Because Journey workflows are durable, our Useless Machine execution can persist across time and reboots.
Let's try it!
Take a note of your execution id (e.g. EXECRDYHBJ8X0RVD8BG0A7A5), and quit iex (Ctrl+C twice), then start a fresh session:
$ iex -S mix
Load the execution by its ID:
iex(1)> execution = Journey.load("EXECRDYHBJ8X0RVD8BG0A7A5"); :ok
:ok
and make sure that the execution is just as we left it:
iex(2)> Journey.values(execution)
%{
execution_id: "EXECRDYHBJ8X0RVD8BG0A7A5",
last_updated_at: 1764110021,
paw: "updated :switch",
switch: "off"
}
Just like before, as soon as you flip the :switch "on", its :paw flips it off:
iex(3)> execution = Journey.set(execution, :switch, "on"); :ok
paw says: 'on? lol no'
:ok
iex(4)> Journey.values(execution)
%{
switch: "off",
paw: "updated :switch",
execution_id: "EXECRDYHBJ8X0RVD8BG0A7A5",
last_updated_at: 1764110367
}
The execution survived a restart. It is just as useless as before. ;)
7. Wrap-up
In under 20 lines of code, we built a (durable, crash-safe, and scalable π!) Useless Machine!
8. Links
Things referenced in this walkthrough:
- Elixir Programming Language: https://elixir-lang.org
- Journey on Hex: https://hexdocs.pm/journey
- Journey on GitHub: https://github.com/markmark206/journey
- Journey's Website: https://gojourney.dev
- Useless Machine on GitHub: https://github.com/markmark206/useless_machine
- Useless Machine gif by Andrei Rudenko: https://dribbble.com/shots/2013355-Useless-Box
Top comments (0)