# How Elixir GenServers, Phoenix LiveView Built a Logic Circuit Simulator  Engineer

Mike Binns

One of the more interesting topics for me in college was logic circuit simulators. You start off with on/off switches and lightbulbs, then introduce gates such as OR, AND, NOT, XOR, etc. From those simple building blocks you can simulate more complex things such as memory by looping an output of two NOR gates, or number displays with a complex mesh of AND/ORs feeding each of the seven segments of the `8`.

Shortly after Phoenix LiveView went public, I saw an advertisement for a mechanical logic circuit simulator, and it got me thinking how a logic circuit simulator would work with Elixir GenServers representing each node, message passing for the outputs/inputs, and a Phoenix LiveView frontend.

In this article, I will review the logic simulatior (`logic_sim`) and the Phoenix LiveView frontend (`logic_sim_liveview`) that resulted from that idea. If you want to jump in and play around with LogicSim first, check out the hosted version here.

## GenServers

First, lets explore what it means to be a `Node` in LogicSim. Not to be confused with the Elixir `Node` module, a LogicSim `Node` represents one simple circuit in LogicSim, such as a switch; lightbulb; or a gate, like and, not, or. A `Node` will have zero or more inputs, zero or more outputs, and the ability to calculate its outputs from its inputs. Some nodes may need to track some sort of inherently internal state (such as an on/off switch), but we should be careful not to store too much state in a `Node` or we risk ruining the fundamental concept that circuits are always built from a small handful of basic building blocks.

A `Node` also needs to track the output nodes it is connected to on each of its outputs, and be able to tell a remote node when its output has changed. On the flip side, a `Node` also needs to listen for incoming messages about any of its inputs, and when it receives a change, recalculate outputs, as well as notify its output nodes.

This concept of tracking state, updating state, and message passing is perfect for GenServers. We are going to put all the common code for a `Node` into the `Node` module, and pull it into individual circuits via `use LogicSim.Node`. The common code accounts for the majority of the code required, so our actual circuits have very little code. For example, here is the complete `Or` circuit:

``````defmodule LogicSim.Node.Or do
use LogicSim.Node, inputs: [:a, :b], outputs: [:a]

def calculate_outputs(_state, %{a: a, b: b} = _input_values) do
%{a: a or b}
end
end
``````

We pass the list of two inputs and one output into the `use LogicSim.Node` call, and simply implement the callback for `calculate_outputs/2`, which gives us the current state and input values map, and takes back the new output values map.

In the code for `OnOffSwitch` we see the use of additional internal state, initialized during the `use LogicSim.Node`, and then destructured from state and used to calculate the output value:

``````defmodule LogicSim.Node.OnOffSwitch do
use LogicSim.Node, outputs: [:a], additional_state: %{on: false}

def toggle(server) do
GenServer.call(server, :toggle)
end

def handle_call(:toggle, _from, %{on: on} = state) do
on = !on
state = %{state | on: on}
state = set_output_value(:a, on, state)
end

def calculate_outputs(%{on: on} = _state, _input_values) do
%{a: on}
end
end
``````

We also see that the `OnOffSwitch` has the ability to “toggle” its internal state via a `GenServer` call, which hooks back into the `Node` function `set_output_value` that handles updating the state and notifying any nodes connected to outputs. Finally, `OnOffSwitch` shows us inputs/outputs are optional.

## Macros

The `__using__` macro in `LogicSim.Node` is what allows us the simplicity of replacing all the boilerplate common code with a call to `use LogicSim.Node`. By injecting the outputs, inputs, and additional state into the `start_link` call with `unquote`, the outside caller to `start_link` only needs to specify the variable opts (e.g. `:listeners`) when starting a node. The `:listeners` list specifies the pids of processes that want to be notified any time the state of the node changes. We will hook into this later when we put a LiveView front end on LogicSim.

``````  defmacro __using__(opts) do
inputs = Keyword.get(opts, :inputs, [])
outputs = Keyword.get(opts, :outputs, [])

quote do
use GenServer
require Logger
@behaviour LogicSim.Node

output_nodes = Enum.reduce(unquote(outputs), %{}, &Map.put(&2, &1, %{}))
output_values = Enum.reduce(unquote(outputs), %{}, &Map.put(&2, &1, false))
input_values = Enum.reduce(unquote(inputs), %{}, &Map.put(&2, &1, false))
listeners = Keyword.get(opts, :listeners, [])

state =
|> Map.put(:inputs, unquote(inputs))
|> Map.put(:outputs, unquote(outputs))
|> Map.put(:output_nodes, output_nodes)
|> Map.put(:output_values, output_values)
|> Map.put(:input_values, input_values)
|> Map.put(:listeners, listeners)

end
``````

Note that we also tag `@behaviour LogicSim.node` inside the macro. This ensures any module that uses `LogicSim.Node` is warned if they don’t implement the required `calculate_outputs` function.

The rest of the code in `LogicView.Node.__using__` revolves around linking and message passing. Each node is responsible for tracking a list of output nodes for each one of its outputs. It does this by associating a map of input pids/inputs for each output. For example, the `:output_nodes` for a node that is connected from its `:a` output to node `0.671.0`’s input `:a` and node `0.675.0`’s input `:b` would look like:

``````%{a: %{#PID<0.671.0> => :a, #PID<0.675.0> => :b}}
``````

Maybe you see in this data model the issue that I just saw as I was writing this article: An output can be connected to two inputs on different nodes, but can one output of a node be connected to multiple inputs on a single node? The answer right now is no, the first connection will get overwritten by the second, so I’ll throw that on the todo list to fix. (Side note: if you ever want to find the issues in your code, write a blog post explaining your code.)

To link two nodes, we call the client function `link_output_to_node`, which calls the nodes GenServer with a `:link_output_to_node` message:

``````      def handle_call(
_from,
%{output_nodes: output_nodes, output_values: output_values} = state
) do

output_nodes = put_in(output_nodes, [output, node], input)

set_node_input(node, input, Map.fetch!(output_values, output))
state = %{state | output_nodes: output_nodes}
send_state_to_listeners(state)
end
``````

Here we add the input node to the list of outputs, noting which input on the input node it is connected to. We then send the current state of this nodes output to the other node via `set_node_input`.

When a node needs to tell a linked node that its input should be updated, it calls the client function `set_node_input`, which casts a `:set_node_input` message to the GenServer. Side note: this was originally a `call` instead of a `cast`, but the first time I created a circuit that looped back on itself, the nodes deadlocked.

``````      def set_node_input(node, input, input_value) do
GenServer.cast(node, {:set_node_input, input, input_value})
end

def handle_cast(
{:set_node_input, input, input_value},
%{
input_values: input_values,
output_values: old_output_values
} = state
) do
if Map.get(input_values, input) != input_value do
input_values = Map.put(input_values, input, input_value)
output_values = calculate_outputs(state, input_values)
state = %{state | input_values: input_values}

state =
output_values
|> Map.keys()
|> Enum.filter(fn key -> old_output_values[key] != output_values[key] end)
|> Enum.reduce(state, fn output, state_acc ->
set_output_value(output, output_values[output], state_acc, false)
end)

send_state_to_listeners(state)
else
end
end
``````

We first check to see if the new input value is actually different from the current one. If it isn’t, then there is no need to do anything as our outputs won’t have changed. If it has changed, we calculate the new output values, and then if that results in any changed outputs, we apply those changes (including recursive calls to `set_node_input` for any connected nodes). Finally we send our new state to any external listeners.

Since the intention of using LogicSim is to display the circuits in some way, we need a way to let external systems know when the state of a node changes. We saw in `use LogicSim.Node` that we can specify a list of external pids via the `:listeners` opt. `send_state_to_listeners`, which we saw used above, simply loops through the list and calls `send` with the current state:

``````      defp send_state_to_listeners(%{listeners: listeners} = state) do
listeners
|> Enum.map(&send(&1, {:logic_sim_node_state, self(), state}))
end
``````

This allows us to hook into LogicSim with LiveView.

## LiveView

I could probably write a full blog post just on the LiveView implementation of LogicSim, but for now I am going to concentrate on the touch points between LogicSim and the LiveView interface.

The design of `LogicSim.Node` makes instantiating a node as simple as calling `start_link!` on the node type you want. Since we want the LiveView process to be notified of any state changes, we pass `self()` in the `:listeners` opt:

``````  def create_node(type, x, y, uuid \\ nil) do
node_state = Node.get_state(node_process)

%{
uuid: uuid || UUID.uuid4(),
type: type,
node_process: node_process,
node_state: node_state,
top: y,
left: x
}
end
``````

Our LiveView implementation tracks its own information alongside the reference to the node process, including top/left coordinates and the current state of the node. This way LiveView can render the state of the node without having to ask it on every render.

## Connecting Nodes

Connecting nodes in LiveView hooks into the above mentioned `link_output_to_node` function:

``````  defp connect_nodes(%{nodes: nodes} = socket, output_uuid, output_output, input_uuid, input_input) do
%{node_process: input_node_process} = get_node_by_uuid(nodes, input_uuid)
%{node_process: output_node_process} = get_node_by_uuid(nodes, output_uuid)

output_node_process,
String.to_existing_atom(output_output),
input_node_process,
String.to_existing_atom(input_input)
)

{:noreply, assign(socket, select_output_for_input: nil, select_input_for_output: nil)}
end
``````

Other than connecting nodes, the only other input into the system is currently by clicking on an `OnOffSwitch`, which simply calls the `toggle` function you saw above:

``````  def handle_node_click(%{type: OnOffSwitch, node_process: node_process}, socket) do
OnOffSwitch.toggle(node_process)
end
``````

The LiveView assigns (and therefore the UI) are updated via the `:logic_sim_node_state` message that we saw above. Since each LiveView process is a GenServer, the message comes in as a `handle_info`:

``````  def handle_info({:logic_sim_node_state, from, node_state}, %{assigns: %{nodes: nodes}} = socket) do
nodes
|> Enum.split_with(fn %{node_process: node_process} -> node_process == from end)
|> case do
{[], _nodes} ->
Logger.warn("Received :logic_sim_node_state for untracked node: #{inspect(from)}")

{[node], nodes} ->
node = %{node | node_state: node_state}
nodes = [node | nodes]
end
end
``````

We simply look up the node in the LiveView state and update our copy of its state in our assigns. This triggers LiveView to re-render the view.

## Click Location

Most of the click interactions in LogicSim LiveView simply use `phx-click="node_click_<%= uuid %>"` on the div to trigger a server side event, with a corresponding `def handle_event("node_click_" <> uuid, _, socket) do` to parse the uuid and handle the click. It gets a bit tricky when you want to figure out where on an element the user clicked. This is necessary to know where to add a new node, or where to move it to. LogicSim LiveView uses the following JavaScript to capture the click event and then set the `phx-value` of the element clicked to the x,y coordinates of the click.

``````window.addEventListener("click", e => {
if (e.target.getAttribute("phx-click") && e.target.getAttribute("phx-send-click-coords")) {
let x = Math.floor(e.clientX - e.target.getClientRects().x);
let y = Math.floor(e.clientY - e.target.getClientRects().y);
let val = `\${x},\${y}`;
e.target.setAttribute("phx-value", val)
}
}, true)
``````

As you can see, it only activates if `phx-send-click-coords="true"` is set on the element. Whenever we want to track where a user is about to click, we put a selection div covering the whole screen, which has `phx-click="selection_mode_div_clicked" phx-send-click-coords="true"`.

We handle that event on the server here:

``````  def handle_event("selection_mode_div_clicked", params, %{assigns: %{selection_mode: selection_mode}} = socket) do
[x, y] = String.split(params, ",")
{x, _} = Integer.parse(x)
{y, _} = Integer.parse(y)
handle_selection_mode_div_clicked(selection_mode, x, y, socket)
end
``````

## The Result That’s about it for this blog post. You can find the full code for LogicSim here, the full code for the LiveView server here, and a deployment of the LiveView server here.

DockYard is a digital product agency offering exceptional strategy, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, San Diego, Phoenix, Dallas, Detroit, Pittsburgh, Baltimore, and New York. 