Type-Safe LiveView
with Gleam
Quinn Wilton / @wilton_quinn / quinnwilton.com
Well-typed programs cannot "go wrong".
- Robin Milner, A Theory of Type Polymorphism in Programming (1978)
A little bit about me...
- Architect & Security Researcher @ Synopsys
- Building automated security analysis tooling for web applications
- Background
- Reverse Engineering
- Malware Analysis
- Competitive CTF player
- Programming Language Theory Enthusiast
- Active contributor to Gleam
Type-system?
- The set of rules used by a programming language to define, detect, and prevent illegal program states using types.
- Erlang / Elixir / Ruby / Python are dynamically typed
- Type violations are caught at runtime rather than compile time
- Haskell / Typescript / C++ / Java are statically typed
- Type violations are caught at compile time
- "Let it crash" means that dynamic typing works really well for Erlang
...then why does erlang need types?
Compile-time documentation
- Functions
- Data Structures
- Errors
Domain Modeling
- Nullability
- Mutual Exclusivity
- Non-Emptiness
Carefree REfactoring
- Exhaustiveness Checking
- Function Signatures
- Decomposing Interfaces
Critical Systems
- Safety Critical
- Mission Critical
- Security Critical
The quality of software depends primarily on the programming methodology in use. [...] A methodology can be easy or difficult to apply in a given language, depending on how well the language constructs match the structures that the methodology deems desirable. [...]
- Barbara Liskov, Abstractions Mechanisms in CLU (1977)
Gleam - Fast, friendly, functional!
- Statically typed functional programming language that compiles to Erlang
- Four principles:
- Be Safe
- Be Performant
- Be Friendly
- Elm style error messages!
- Be a Good Citizen
- Easy interop with BEAM!
What can gleam do now?
- Almost anything Erlang can do
-
Libraries exist for:
- Web Servers (Plug, Elli, Cowboy, Lean)
- HTTP Clients (httpc)
- JSON (jsone)
- Postgres (pgo)
- Templating (mustache)
- Midas is a web framework
- Plum Mail — a Y Combinator funded company
- A wrapper for OTP is in progress
import gleam/int
import gleam/string
pub opaque type Counter {
Counter(value: Int, step: Int)
}
pub type Msg {
Increment
Decrement
Reset
}
pub fn init(step: Int) -> Counter {
Counter(value: 0, step: step)
}
pub fn update(counter: Counter, msg: Msg) -> Counter {
case msg {
Increment -> Counter(..counter, value: counter.value + counter.step)
Decrement -> Counter(..counter, value: counter.value - counter.step)
Reset -> init(counter.step)
}
}
pub fn render(counter: Counter) -> String {
string.append("The current count is: ", int.to_string(counter.value))
}
init(Step) ->
{counter, 0, Step}.
update(Counter, Msg) ->
case Msg of
increment ->
erlang:setelement(
2,
Counter,
erlang:element(2, Counter)
+ erlang:element(3, Counter)
);
decrement ->
erlang:setelement(
2,
Counter,
erlang:element(2, Counter)
- erlang:element(3, Counter)
);
reset ->
init(erlang:element(3, Counter))
end.
render(Counter) ->
gleam@string:append(
<<"The current count is: "/utf8>>,
gleam@int:to_string(erlang:element(2, Counter))
).
-record(counter, {count, step}).
It's all about layers!
- Core
- Data
- Functions
- Tests
- Boundary
- Processes
- Supervisors
- Workers
Sometimes I think the only universal in the computing field is the fetch-execute cycle.
- Alan J. Perlis, Epigrams on Programming (1982)
CHIP-8
- Interpreted programming language from the mid 1970s
- Designed to run on the "CHIP-8 Virtual Machine"
- 4K of RAM
- 16 registers
- 48 byte stack
- Two 60Hz timers
- 16 key hex keyboard
- 64x32 pixel screen
- 35 opcodes
Demo Time
pub type State {
Running
AwaitingROM
AwaitingInput(vx: registers.DataRegister)
}
pub type Command {
SoundOn
SoundOff
}
pub type Emulator {
Emulator(
state: State,
registers: registers.RegisterFile,
keyboard: keyboard.Keyboard,
pc: Int,
stack: stack.Stack,
memory: memory.Memory,
screen: screen.Screen,
)
}
pub fn init() -> Emulator {
Emulator(
state: AwaitingROM,
registers: registers.new(),
keyboard: keyboard.new(),
pc: 0x200,
stack: stack.new(),
memory: memory.put(memory.new(4096), 0, font),
screen: screen.new(64, 32),
)
}
import gleam/bit_string
import chip8/helpers
pub opaque type Memory {
Memory(data: BitString)
}
pub fn new(size: Int) -> Memory {
Memory(data: helpers.bitstring_copy(<<0>>, size))
}
pub fn put(memory: Memory, position: Int, data: BitString) -> Memory {
assert Ok(left) = bit_string.part(memory.data, 0, position)
assert Ok(right) =
bit_string.part(
memory.data,
position + bit_string.byte_size(data),
bit_string.byte_size(memory.data) - position - bit_string.byte_size(data),
)
let data =
left
|> bit_string.append(data)
|> bit_string.append(right)
Memory(data: data)
}
pub fn read(
memory: Memory,
position: Int,
length: Int,
) -> Result(BitString, Nil) {
bit_string.part(memory.data, position, length)
}
import gleam/int
import gleam/string
pub external fn bitstring_copy(subject: BitString, n: Int) -> BitString =
"binary" "copy"
pub external fn bitstring_to_list(subject: BitString) -> List(Int) =
"binary" "bin_to_list"
pub external fn rand_uniform(n: Int) -> Int =
"rand" "uniform"
pub fn int_to_hex_string(n: Int) -> String {
string.append("0x", int.to_base_string(n, 16))
}
defmodule Chip8.Emulator do
use Strucord,
name: :emulator,
from: "gen/src/chip8@emulator_Emulator.hrl"
alias __MODULE__
def init() do
from_record(:chip8@emulator.init())
end
def reset(%Emulator{} = emulator) do
with_record(emulator, fn record ->
:chip8@emulator.reset(record)
end)
end
def load_rom(%Emulator{} = emulator, rom) when is_binary(rom) do
with_record(emulator, fn record ->
:chip8@emulator.load_rom(record, rom)
end)
end
def step(%Emulator{} = emulator) do
{emulator, commands} = :chip8@emulator.step(to_record(emulator))
{from_record(emulator), commands}
end
def handle_timers(%Emulator{} = emulator) do
{emulator, commands} = :chip8@emulator.handle_timers(to_record(emulator))
{from_record(emulator), commands}
end
end
defmodule Chip8Web.PageLive do
use Chip8Web, :live_view
def mount(_params, _session, socket) do
emulator = Chip8.Emulator.init()
socket = assign(socket, emulator: Emulator.init())
{:ok, socket}
end
def handle_event("load_rom", %{"name" => rom_name}, socket) do
rom = Chip8.ROMs.get_rom(rom_name)
emulator =
Chip8.Emulator.load_rom(
socket.assigns.emulator,
rom.data
)
socket = assign(socket, emulator: emulator)
{:noreply, socket}
end
def handle_event("step", _value, socket) do
{emulator, commands} = Chip8.Emulator.step(socket.assigns.emulator)
socket =
socket
|> assign(:emulator, emulator)
|> handle_commands(commands)
{:noreply, socket}
end
end
Hooks.SoundCard = {
mounted() {
this.synth = createAudio();
this.handleEvent("enable_soundcard", () => {
this.synth.on();
});
this.handleEvent("disable_soundcard", () => {
this.synth.off();
});
},
beforeDestroy() {
this.synth.close();
},
disconnected() {
this.synth.off();
}
}
defp handle_commands(socket, commands) do
Enum.reduce(commands, socket, fn command, socket ->
case command do
:sound_on -> push_event(socket, "enable_soundcard", %{})
:sound_off -> push_event(socket, "disable_soundcard", %{})
end
end)
end
<div phx-window-keyup="key_up">
<div phx-window-keydown="key_down" phx-throttle="100">
<div class="grid-container">
<div class="window window-screen">
<h1 class="header">Screen</h1>
<div class="body">
<svg id="screen" class="screen" viewBox="0 0 64 32">
<%= for {x, y, opacity} <- :chip8@screen.to_list(@emulator.screen) do %>
<%= if opacity > 0 do %>
<rect x="<%= x %>" y="<%= y %>" width="1" height="1" opacity="<%= opacity %>"/>
<% end %>
<% end %>
</svg>
</div>
</div>
<div>
pub type Pixel {
OnPixel
OffPixel
DecayingPixel(lifetime: Float)
}
pub opaque type Screen {
Screen(width: Int, height: Int, contents: List(List(Pixel)))
}
pub fn new(width: Int, height: Int) -> Screen {
Screen(
width: width,
height: height,
contents: list.repeat(list.repeat(OffPixel, width), height),
)
}
pub type Instruction {
ClearScreen
ReturnFromSubroutine
JumpRelative(offset: Int)
SkipNextIfEqualConstant(vx: registers.DataRegister, value: Int)
...
}
pub fn decode_instruction(instruction: BitString) -> Instruction {
case instruction {
<<0:4, 0:4, 0xE:4, 0:4>> -> ClearScreen
<<0:4, 0:4, 0xE:4, 0xE:4>> -> ReturnFromSubroutine
<<0xB:4, nnn:12>> -> JumpRelative(nnn)
<<3:4, x:4, kk:8>> ->
SkipNextIfEqualConstant(registers.to_data_register(x), kk)
...
}
}
pub fn execute_instruction(
emulator: Emulator,
instruction: instruction.Instruction,
) -> Emulator {
case instruction {
instruction.ClearScreen ->
Emulator(..emulator, screen: screen.clear(emulator.screen))
instruction.JumpAbsolute(address) ->
Emulator(..emulator, pc: address - 2)
...
}
}
LiveView + Gleam = ❤️
Thank you!
Quinn Wilton / @wilton_quinn / quinnwilton.com
github.com/QuinnWilton/gleam-chip8
gleam.run/
chip8.quinnwilton.com
Type-Safe LiveView with Gleam
By quinnwilton
Type-Safe LiveView with Gleam
- 586