Gleam Bit Arrays

Bit arrays store raw binary data — sequences of bits and bytes. They are essential for working with network protocols, file formats, cryptographic hashes, image data, and anything involving bytes rather than text. Gleam inherits Erlang's powerful bit syntax, making binary operations both expressive and safe.

What Is a Bit Array?


Bit Array vs String
──────────────────────────────────────────────────
String:    "hello"   → UTF-8 encoded text
                       human-readable
                       use for names, messages, labels

BitArray:  <<104, 101, 108, 108, 111>>
                       raw bytes
                       use for files, protocols, hashes

"hello" and <<104,101,108,108,111>> contain the same
bytes, but one is text and one is binary data.

Creating Bit Arrays

// Byte literals
let bytes = <<72, 101, 108, 108, 111>>   // "Hello" as bytes

// From a string (UTF-8 encoding)
let from_string = <<"Hello":utf8>>

// Single bits and values
let flags = <<1:1, 0:1, 1:1>>    // 3 bits: 1, 0, 1
let port  = <<8080:16>>           // 16-bit integer
let uuid  = <<0:128>>             // 128 zero bits

Bit Array Literal Syntax
──────────────────────────────────────────────────
<>              → 8-bit integer (default)
<>         → value using exactly `size` bits
<>         → value as UTF-8 encoded bytes
<>            → multiple segments joined

<<72, 101, 108>>
  72  = 0x48 = 'H'
  101 = 0x65 = 'e'
  108 = 0x6C = 'l'

Bit Array Segments

Each segment inside << >> can specify size, type, and endianness:


Segment Options
──────────────────────────────────────────────────
<>       → N bits wide
<>             → 8 bits (1 byte)
<>            → 16 bits (2 bytes)
<>            → 32 bits (4 bytes)
<>            → 64 bits (8 bytes)

<>        → little-endian byte order
<>           → big-endian byte order (default)
<>        → system byte order

<>         → IEEE 754 float
<>          → UTF-8 string
<>         → UTF-16 string
<>          → any number of bits

Pattern Matching on Bit Arrays

The most powerful feature of Erlang's bit syntax — and Gleam inherits it — is pattern matching on binary structures:

// Parse a simple 4-byte IPv4 address
pub fn parse_ipv4(data: BitArray) -> Result(#(Int,Int,Int,Int), Nil) {
  case data {
    <> -> Ok(#(a, b, c, d))
    _                       -> Error(Nil)
  }
}

// parse_ipv4(<<192, 168, 1, 1>>) → Ok(#(192, 168, 1, 1))

IPv4 Pattern Match
──────────────────────────────────────────────────
data = <<192, 168, 1, 1>>

<>
  a = 192  (first 8 bits)
  b = 168  (next 8 bits)
  c = 1    (next 8 bits)
  d = 1    (last 8 bits)

Result: #(192, 168, 1, 1)

Reading a Binary Protocol Header

// A simple message format:
// [version: 8 bits][type: 8 bits][length: 16 bits][payload: length bytes]

type MessageHeader {
  MessageHeader(version: Int, msg_type: Int, length: Int)
}

pub fn parse_header(data: BitArray) -> Result(#(MessageHeader, BitArray), Nil) {
  case data {
    <> ->
      Ok(#(MessageHeader(version, msg_type, length), rest))
    _ -> Error(Nil)
  }
}

Protocol Header Layout
──────────────────────────────────────────────────
Byte:  0        1        2-3       4+
      ┌────────┬────────┬──────────┬──────────────┐
      │version │  type  │  length  │   payload    │
      │ 8 bits │ 8 bits │ 16 bits  │ length bytes │
      └────────┴────────┴──────────┴──────────────┘

Building Bit Arrays

// Build a 4-byte header
pub fn build_header(version: Int, msg_type: Int, payload: BitArray) -> BitArray {
  let length = bit_array.byte_size(payload)
  <>
}

let header = build_header(1, 42, <<"Hello":utf8>>)
// <<1, 42, 0, 5, 72, 101, 108, 108, 111>>

Common Bit Array Functions

import gleam/bit_array

bit_array.byte_size(ba)         // number of bytes
bit_array.bit_size(ba)          // number of bits
bit_array.to_string(ba)         // convert to String (Result)
bit_array.from_string(s)        // convert from String
bit_array.concat([a, b, c])     // join multiple bit arrays
bit_array.slice(ba, from, size) // extract a portion
bit_array.inspect(ba)           // hex debug string

Practical Example — Simple Checksum

import gleam/bit_array
import gleam/list

pub fn checksum(data: BitArray) -> Int {
  data
  |> bit_array.to_list     // convert to List(Int) of bytes
  |> list.fold(0, fn(acc, byte) { acc + byte })
  |> fn(sum) { sum % 256 }  // 8-bit checksum
}

pub fn verify(data: BitArray, expected: Int) -> Bool {
  checksum(data) == expected
}

pub fn main() {
  let payload = <<"Hello":utf8>>
  let cs = checksum(payload)
  io.debug(cs)                              // some checksum value
  io.debug(verify(payload, cs))             // True
  io.debug(verify(<<"World":utf8>>, cs))    // False
}

Bit Arrays and Strings


Converting Between BitArray and String
──────────────────────────────────────────────────
String → BitArray:
  bit_array.from_string("hello")
  → <<104, 101, 108, 108, 111>>

BitArray → String (may fail if not valid UTF-8):
  bit_array.to_string(<<104, 101, 108, 108, 111>>)
  → Ok("hello")

  bit_array.to_string(<<0xFF, 0xFE>>)
  → Error(Nil)  (not valid UTF-8)

Key Points


Bit Array Essentials
──────────────────────────────────────────────────
1. BitArray stores raw binary data (bytes and bits)
2. Create with << >> literal syntax
3. Segment options: size, type (utf8, float), endianness
4. Pattern match on binary structures with case
5. Use for protocols, file formats, hashes, network data
6. gleam/bit_array module for utility functions
7. Convert to/from String with from_string / to_string

Bit arrays make Gleam a first-class language for systems programming and network applications. The ability to pattern match directly on binary structures — parsing a TCP packet, reading a file header, or decoding a binary protocol — without writing a single byte-shifting loop is one of the most powerful features Gleam inherits from Erlang.

Leave a Comment

Your email address will not be published. Required fields are marked *