ferrule

worked examples

α1

worked examples


Parsing with Refinements

type Port = u16 where self >= 1 && self <= 65535;

error Invalid { message: String }
domain ParseError = Invalid;

use error ParseError;

function parsePort(s: String) -> Port error ParseError {
  const trimmed = text.trim(s);
  const n = check number.parse_u16(trimmed) with { op: "parse_u16" };
  
  if n < 1 || n > 65535 { 
    return err Invalid { message: "port out of range" };
  }
  
  return ok Port(n);
}

File I/O with Capabilities

error NotFound { path: Path }
error Denied { path: Path }

domain IoError = NotFound | Denied;

function readAll(path: Path, cap fs: Fs) -> Bytes error IoError effects [fs] {
  const file = check fs.open(path);
  return check fs.readAll(file);
}

function writeConfig(config: Config, path: Path, cap fs: Fs) -> Unit error IoError effects [fs] {
  const data = serialize(config);
  return check fs.writeAll(path, data);
}

Error Composition with Pick/Omit

error NotFound { path: Path }
error Denied { path: Path }
error Timeout { ms: u64 }
error ParseFailed { line: u32, message: String }

domain IoError = NotFound | Denied | Timeout;
domain ParseError = ParseFailed;
domain AppError = IoError | ParseError;

function loadConfig(p: Path, cap fs: Fs) -> Config error AppError effects [fs] {
  const bytes = map_error readAll(p, fs) 
                using (e => e);  // IoError is subset of AppError
  
  return map_error parser.config(bytes) 
         using (e => e);  // ParseError is subset of AppError
}

// function with precise errors using Pick
function quickRead(path: Path, cap fs: Fs) -> Bytes error Pick<IoError, NotFound | Denied> effects [fs] {
  // guaranteed not to timeout
}

Polymorphism with Records

// operation records
type Hasher<T> = { 
  hash: (T) -> u64, 
  eq: (T, T) -> Bool 
};

type Showable<T> = { 
  show: (T) -> String 
};

// type definition
type UserId = { id: u64 };

// implementations as namespaced constants
const UserId.hasher: Hasher<UserId> = {
  hash: function(u: UserId) -> u64 { return u.id; },
  eq: function(a: UserId, b: UserId) -> Bool { return a.id == b.id; }
};

const UserId.show: Showable<UserId> = {
  show: function(u: UserId) -> String { 
    return "User(" ++ u64.toString(u.id) ++ ")"; 
  }
};

// generic function
function dedupe<T>(items: View<T>, h: Hasher<T>) -> View<T> effects [alloc] {
  // use h.hash(item), h.eq(a, b)
}

// usage — explicit
const unique = dedupe(users, UserId.hasher);

HTTP Client with Timeout

error Timeout { url: Url, ms: u64 }
error Network { message: String }
error BadStatus { code: u32 }

domain FetchError = Timeout | Network | BadStatus;

function fetch(
  url: Url, 
  deadline: Time,
  cap net: Net,
  cap clock: Clock
) -> Response error FetchError effects [net, time] {
  const tok = cancel.token(deadline);
  
  const sock = map_error net.connect(url.host, url.port, tok)
               using (function(e: NetError) -> FetchError { 
                 return Timeout { url: url, ms: time.until(deadline) };
               });
  
  return check request(sock, url, tok) with { op: "request" };
}

Parallel Processing

function processAll(
  items: View<Item>,
  cap fs: Fs,
  cap clock: Clock
) -> View<Result> error ProcessError effects [fs, time, alloc] {
  
  const deadline = clock.now() + Duration.seconds(30);
  
  return task.scope(function(scope: Scope) -> View<Result> error ProcessError {
    const results = builder.new<Result>(region.current());
    
    for item in items {
      scope.spawn(function() -> Unit error ProcessError {
        const result = check processItem(item, fs);
        builder.push(results, result);
      });
    }
    
    check scope.awaitAll();
    return ok builder.finish(results);
  });
}

Region Management

// copy to caller's region
function cloneToRegion(src: View<u8>, dstRegion: Region) -> View<u8> effects [alloc] {
  return view.copy(src, to = dstRegion);
}

// return region along with view
function cloneWithRegion(src: View<u8>) -> { data: View<u8>, region: Region } effects [alloc] {
  const heap = region.heap();
  const dst = view.copy(src, to = heap);
  return { data: dst, region: heap };
}

// arena for temporary allocations
function processWithArena(input: View<u8>) -> Output effects [alloc] {
  const arena = region.arena(1 << 20);  // 1MB
  defer arena.dispose();
  
  const temp = arena.alloc<u8>(input.len * 2);
  // use temp for intermediate work...
  
  // copy result to heap before arena is disposed
  const result = view.copy(output, to = region.heap());
  return result;
}

Compile-Time Table Generation

comptime function crc16Table(poly: u16) -> Array<u16, 256> {
  var table: Array<u16, 256> = [0; 256];
  var i: u32 = 0;
  
  while i < 256 {
    var crc: u16 = u16(i) << 8;
    var j: u32 = 0;
    
    while j < 8 {
      if (crc & 0x8000) != 0 {
        crc = (crc << 1) ^ poly;
      } else {
        crc = crc << 1;
      }
      j = j + 1;
    }
    
    table[i] = crc;
    i = i + 1;
  }
  
  return table;
}

const CRC16_TABLE = comptime crc16Table(0x1021);

function crc16(data: View<u8>) -> u16 {
  var crc: u16 = 0xFFFF;
  
  for byte in data {
    const idx = ((crc >> 8) ^ u16(byte)) & 0xFF;
    crc = (crc << 8) ^ CRC16_TABLE[usize(idx)];
  }
  
  return crc;
}

Context Ledgers

function handleRequest(req: Request, cap fs: Fs, cap net: Net) -> Response error AppError effects [fs, net] {
  with context { request_id: req.id, user_id: req.user } in {
    const config = check loadConfig(fs);
    const data = check fetchExternal(req.url, net);
    
    // all errors within this block carry request_id and user_id
    return ok processData(data, config);
  }
}

Generics with Variance

// covariant — can only output T
type Producer<out T> = { get: () -> T };

// contravariant — can only input T  
type Consumer<in T> = { accept: (T) -> Unit };

// Producer<Cat> is assignable to Producer<Animal>
function printAnimal(p: Producer<Animal>) -> Unit effects [io] {
  io.println(p.get().name);
}

const catProducer: Producer<Cat> = { get: function() -> Cat { return myCat; } };
printAnimal(catProducer);  // OK: Cat is Animal

Conditional Types

type Unwrap<T> = if T is Result<infer U, infer E> then U else T;

type UnwrappedConfig = Unwrap<Result<Config, Error>>;  // Config

type ElementType<A> = if A is Array<infer T, infer N> then T else Never;

type StringElement = ElementType<Array<String, 10>>;  // String

Higher-Order Functions with Effect Spread

function map<T, U>(arr: View<T>, f: (T) -> U) -> View<U> effects [alloc, ...] {
  const result = builder.new<U>(region.current());
  for item in arr {
    builder.push(result, f(item));
  }
  return builder.finish(result);
}

// caller's effects include alloc + whatever the passed function has
function processAll(items: View<Item>, cap fs: Fs) -> View<Output> effects [alloc, fs] {
  return map(items, function(item: Item) -> Output effects [fs] {
    return check fs.process(item);
  });
}

On this page