Skip to content

Data transformation

Immutable bindings, shadowing, destructuring, and first-argument pipes for readable data-first code.

Updated View as Markdown

Hem uses an immutable, data-first style. You bind a value, transform it, and bind the next stage without mutating the previous value. The emitted program compiles to standard TypeScript.

Immutable stages and shadowing

let emits const. Rebinding a name is allowed; Hem keeps the first binding’s name and gives subsequent bindings deterministic $n suffixes in emitted TypeScript.

let readPort = (env: Env): number => {
  let port = env.PORT
  let Some(port) = port else return 8080
  let port = Number.parseInt(port, 10)
  if Number.isNaN(port) { 8080 } else { port }
}

Each binding can change the type: port goes from ?string to string to number. Shadowing is lexical: a later binding references the previous binding, and closures retain the binding they captured. For same-type steps, chaining or |> is usually clearer.

Destructuring at the boundary

Object and array destructuring follow TypeScript syntax. Destructure when a function or stage needs specific fields, and keep the rest when the next stage needs the remainder.

let { id, profile: { displayName } } = user
let [head, ...tail] = events
let summary = `${id}: ${displayName} (${tail.length + 1} events)`

Destructuring is also available in arrow parameters and in pattern bindings. The type checker validates annotated TypeScript types, including the ?T produced by unchecked indexing.

First-argument pipes

x |> f emits f(x). When called with arguments, Hem inserts the piped value into the first argument position: x |> f(a) emits f(x, a). The pipe has no runtime helper and does not curry arguments.

let slug = title |> normalize |> truncate(80) |> encodeURIComponent

This corresponds to the direct call:

const slug = encodeURIComponent(truncate(normalize(title), 80));

Libraries with fixed-arity, data-first functions use the same call signatures in both direct and piped code.

Later arguments and partial application

The pipe operator inserts only into the first argument position. Wrap calls that require later arguments in an explicit arrow function so argument order remains clear.

let label = (prefix: string, value: number) => `${prefix}${value}`
let withLabel = (value: number) => label("value: ", value)
let render = (value: number) => value |> withLabel

Use the same explicit arrow syntax for partial application:

let withValue = (value: number) => label("value: ", value)

Hem does not infer curried functions or automatically pass a piped value to a later parameter.

Arrows only

Functions always use arrow syntax: there is no function keyword and no automatic currying. Generic parameters, defaults, rest parameters, and async are supported. The checker can infer a type predicate from a checked implementation; explicitly authored is and asserts signatures are refused with HEM2029 because the signature does not prove the implementation. Imported predicate and assertion contracts remain usable. See TypeScript interop.

let map = <A, B>(items: A[], transform: (item: A) => B): B[] => items.map(transform)
let names = map(users, (user) => user.name)

When a function requires named inputs, pass a standard options object. This is explicit in Hem and uses the same object shape in TypeScript.

Normative source

Specification 0.2, §4 (bindings), §5 (functions), §6 (pipes), and §13 (modules and interop). The tour’s §§2, 4, and 9 show the corresponding source and emitted forms.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close