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