Errow

Hex.pm License

Errow (Erlang + Arrow) provides zero-copy, in-memory columnar arrays for Elixir and the BEAM, fully integrated with Elixir protocols (Enumerable, Collectable, Access) and 100% byte-for-byte compliant with the Apache Arrow Columnar Specification and Arrow C Data Interface.


Why Errow?

Standard Elixir collections are linked lists ([head | tail]) or tree structures. While excellent for general functional programming, they have limitations for data-intensive and interop workloads:

Errow solves this by storing data in continuous BEAM binaries:


Installation

Add errow to your list of dependencies in mix.exs:

def deps do
  [
    {:errow, "~> 0.1.0"}
  ]
end

Quickstart

1. Strings (Errow.String)

# Build in comprehensions with Collectable
names = for name <- ["Elixir", "BEAM", "Arrow", "Errow"], into: Errow.string() do
  String.upcase(name)
end

# O(1) Indexing via Access
names[0]    # => "ELIXIR"
names[-1]   # => "ERROW" (negative indices supported!)
names[1..2] # => ["BEAM", "ARROW"]

# In-place updates via Access
updated = put_in(names[1], "ZIG")
updated[1]  # => "ZIG"

# Enumerable & Stream
Enum.map(names, &byte_size/1) # => [6, 4, 5, 5]

2. Numerics with 128-bit and 256-bit BigInts (Errow.Numeric)

Errow.Numeric supports fixed-width primitive types:

# 32-bit signed integers (compact: 4 bytes per item)
ints = Errow.numeric([10, 20, 30, 40], :s32)
Enum.sum(ints) # => 100

# 128-bit BigInts (Arrow Decimal128 compatible, exactly 16 bytes per item)
big128 = Errow.numeric([300_000_000_000_000_000_000_000_000_000_000_000], :s128)
big128[0] # => 300000000000000000000000000000000000

# 256-bit BigInts (Arrow Decimal256 / Web3 uint256, exactly 32 bytes per item)
u256 = Errow.numeric([115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935], :u256)

3. Raw Binary Blobs (Errow.Binary)

For raw, opaque byte buffers (hashes, UUIDs, PNG/JPEG payloads, Protobuf messages):

blobs = Errow.binary([<<1, 2, 3>>, <<255, 254>>, nil])
blobs[0] # => <<1, 2, 3>>
blobs[2] # => nil

Apache Arrow C Data Interface (Zero-Copy Interop)

Exporting your Elixir arrays to native libraries in C, Rust, Zig, or DuckDB requires zero memory allocations:

arr = Errow.string(["Daniel", "Kuku", "Elixir"])

# 1. Export schema metadata
schema = Errow.CData.export_schema(arr, name: "username")
schema.format     # => "u" (Arrow Utf8 format)
schema.n_buffers  # => 3

# 2. Export 8-byte aligned raw binary buffers
%{length: 3, null_count: 0, buffers: [validity, offsets, data]} = Errow.CData.export_buffers(arr)

Consuming in Rust (Rustler + arrow-rs):

use rustler::{Binary, NifResult};
use arrow::array::StringArray;
use arrow::buffer::{Buffer, OffsetBuffer};

#[rustler::nif]
fn inspect_arrow<'a>(_validity: Binary<'a>, offsets_bin: Binary<'a>, data_bin: Binary<'a>) -> NifResult<usize> {
    // Zero-copy wrap BEAM binaries in Arrow buffers:
    let data_buf = Buffer::from_slice_ref(data_bin.as_slice());
    let offsets_slice: &[i32] = bytemuck::cast_slice(offsets_bin.as_slice());
    let offset_buf = OffsetBuffer::new(offsets_slice.to_vec().into());

    let string_array = unsafe {
        StringArray::new_unchecked(offset_buf, data_buf, None)
    };

    Ok(string_array.len())
}

Consuming in C (erl_nif.h):

#include <erl_nif.h>
#include <stdint.h>

static ERL_NIF_TERM get_string_len(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifBinary offsets_bin, data_bin;
    enif_inspect_binary(env, argv[0], &offsets_bin);
    enif_inspect_binary(env, argv[1], &data_bin);

    const int32_t* offsets = (const int32_t*) offsets_bin.data;
    uint32_t start = offsets[0];
    uint32_t end = offsets[1];
    
    // Direct pointer to UTF-8 characters without copying!
    const char* str = (const char*) (data_bin.data + start);
    return enif_make_uint(env, end - start);
}

Consuming in Python (pyarrow):

import pyarrow as pa

# Zero-copy wrap BEAM binary buffers exported by Errow.CData.export_buffers/2:
buf_offsets = pa.py_buffer(offsets_bytes)
buf_data = pa.py_buffer(data_bytes)
buf_validity = pa.py_buffer(validity_bytes) if validity_bytes else None

arrow_array = pa.Array.from_buffers(
    pa.string(),
    length,
    [buf_validity, buf_offsets, buf_data],
    null_count=null_count
)

Testing & Specification Compliance

Errow is verified against official Apache Arrow test fixtures from apache/arrow-testing:


License

Errow is licensed under the Apache-2.0 License.