Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Higher-Kinded Types

Ordinary generics abstract over a complete type such as I64. Higher-kinded types abstract over a type constructor such as Option, Vec, or a partially applied Result. Rock’s prelude provides standalone functions and consuming receiver methods for working with these constructors; ordinary calls do not need a qualified trait path.

Kinds and Constructor Holes

The notation below is type-level documentation, not a Rock program:

I64               : Type
Option            : Type -> Type
Result            : Type -> Type -> Type
Result _, I64     : Type -> Type

Option waits for one type argument. Result _, I64 fixes the error type to I64 and waits for its success type. The underscore is a constructor hole, not a runtime value.

Functor and F _

The Functor trait maps a callback over a constructor while preserving its shape. The bound F _: Functor says that F is a unary constructor with a Functor implementation.

map_any: M -> F A -> F B where F _: Functor, M: FnMut A, B
map_any = mapper, value -> fmap mapper, value

increment: I64 -> I64
increment = value -> value + 1

double: I64 -> I64
double = value -> value * 2

make_values: () -> Vec I64
make_values = ->
    mut values = Vec::new!
    values.push 1
    values.push 2
    values.push 3
    values

main = !->
    option_result = map_any increment, Option::Some 4
    vector_result = make_values!.fmap double
    option_result.println!
    vector_result.println!

At the first call, F = Option, A = I64, and B = I64; at the second, the receiver supplies F = Vec. The standalone fmap mapper, value and receiver form value.fmap mapper delegate to the same constructor’s Functor implementation. The output is Some(5) and [2, 4, 6]. Both input carriers are consumed by their mapping operation.

For Result, a function signature can fix the error type while the call still infers the constructor:

map_result: M -> Result A, I64 -> Result B, I64 where M: FnMut A, B
map_result = mapper, value -> fmap mapper, value

increment: I64 -> I64
increment = value -> value + 1

main = !->
    success = map_result increment, Result::Ok 4
    failure = map_result increment, Result::Err 9
    success.println!
    failure.println!

Here the inferred constructor is Result _, I64: the error type stays fixed while fmap changes the success payload. It has the required unary kind, while bare Result would still require two arguments. The output is Ok(5) and Err(9). When explicit dispatch is useful, the same operation can be written (Result _, I64)::Functor::fmap mapper, value; that qualification is not required at ordinary call sites.

Applicative Values

Applicative adds pure for lifting a value and ap for applying a wrapped function to a wrapped argument.

repure_any: F I64 -> F I64 where F _: Applicative
repure_any = ignored -> pure 2

apply_any: F (I64 -> I64) -> F I64 -> F I64 where F _: Applicative
apply_any = wrapped_function, wrapped_value ->
    ap wrapped_function, wrapped_value

increment: I64 -> I64
increment = value -> value + 1

main = !->
    lifted = repure_any Option::Some 0
    wrapped = Option::Some increment
    applied = apply_any wrapped, lifted
    applied.println!

The inferred constructor is F = Option. repure_any ignores its input carrier and calls pure 2, producing Some 2; then ap applies increment, producing Some 3. The output is Some(3). The receiver form is wrapped_function.ap wrapped_value; its receiver is the carrier holding the function.

Unlike fmap, pure has no input carrier from which to infer its constructor. Its expected return type must supply that information. In repure_any, the return signature supplies F I64, and the caller fixes F through the argument. A concrete return signature works too:

make_option: () -> Option I64
make_option = -> pure 2

make_result: () -> Result I64, I64
make_result = -> pure 3

main = !->
    make_option!.println!
    make_result!.println!

This prints Some(2) and Ok(3). Without such context, pure 2 cannot choose a carrier. pure remains a standalone or associated function because it creates a carrier rather than consuming an existing one.

Monad Binding

Monad sequences a carrier with a callback that returns another value of the same constructor.

bind_any: F I64 -> M -> F I64 where F _: Monad, M: FnMut I64, (F I64)
bind_any = value, callback -> value.bind callback

add_two: I64 -> Option I64
add_two = value -> Option::Some value + 2

main = !->
    start = Option::Some 3
    result = bind_any start, add_two
    result.println!

Here F = Option and M is the callback type I64 -> Option I64. bind unwraps Some 3, calls add_two, and returns Some 5. The output is Some(5); a None input would skip the callback. The standalone spelling is bind value, callback.

Foldable and Traversable

Foldable consumes a constructor from left to right with a state and a step function. Traversable maps every element into an effect while rebuilding the original shape inside that effect.

fold_digits: (I64, I64) -> I64
fold_digits = pair -> pair.0 * 10 + pair.1

increment_effect: I64 -> Option I64
increment_effect = value -> Option::Some value + 1

make_values: () -> Vec I64
make_values = ->
    mut values = Vec::new!
    values.push 1
    values.push 2
    values.push 3
    values

main = !->
    option_total = foldl fold_digits, 0, Option::Some 4
    vector_total = make_values!.foldl fold_digits, 0
    traversed = make_values!.traverse increment_effect
    option_total.println!
    vector_total.println!
    traversed.println!

The fold over Some 4 returns 4; the vector fold computes 123; and traversal produces Some([2, 3, 4]). The output is 4, 123, and Some([2, 3, 4]). Vec supplies Functor, Foldable, and Traversable; its traversal consumes the input vector while constructing a new vector. The standalone traversal spelling is traverse increment_effect, make_values!.

traverse_m uses monadic binding instead of applicative application and can stop as soon as an effect fails.

stop_at_two: I64 -> Option I64
stop_at_two = value ->
    if value == 2
        Option::None
    else
        Option::Some value

make_values: () -> Vec I64
make_values = ->
    mut values = Vec::new!
    values.push 1
    values.push 2
    values.push 3
    values

main = !->
    result = make_values!.traverse_m stop_at_two
    result.println!

The second callback returns None, so traversal stops and the output is None. The standalone spelling is traverse_m stop_at_two, make_values!.

Traversing for Effects

Use the standalone for_each values, action when the callback exists for effects rather than to build another container. The action returns () and may mutate its captures through FnMut:

main = !->
    mut total = 0
    for_each 1..=3, number !->
        total = total + number
    total.println!

    for_each (Option::Some 7), (!.println!)
    result: Result I64, I64 = Result::Ok 9
    for_each result, (!.println!)

The output is 6, 7, and 9. Option::None and Result::Err invoke no callback. A Vec T supplies each owned element in order and is consumed; &Vec T and &[T] supply shared references while preserving the owner. Borrow an array as a slice to use the same function.

The public function uses the ordinary ForEach T bridge trait. Its blanket implementation for F T where F _: Foldable derives traversal from F::Foldable::foldl: the callback is carried as fold state, called once per element, and returned for the next step. Any new constructor implementing Foldable receives this behavior automatically; no separate ForEach implementation is needed.

Native Range is a concrete integer type, not a unary type constructor. Its dedicated bridge implementation visits bounded ranges directly without allocating a vector. Exclusive start..end and inclusive start..=end ranges run in ascending order; reversed ranges are empty, and an inclusive equal-endpoint range visits one integer. Open-ended ranges terminate with for_each requires a bounded range before invoking the action.

(!.println!) is a unit-returning receiver section: it means number !-> number.println!. The leading ! discards the result of the whole section body, while (.println!) would return the method’s result.

Sequencing Effects

sequence is the standard Traversable operation for turning T (G A) into G (T A). The following exact stdlib pattern sequences a vector of options.

main = !->
    mut effects = Vec::new!
    effects.push Option::Some 4
    effects.push Option::Some 5
    successful = sequence effects
    match successful
        Option::Some values =>
            values.len!.println!
            match values.get 0
                Option::Some value => *value .println!
                Option::None => 0.println!
        Option::None => 0.println!

    mut failed_effects = Vec::new!
    failed_effects.push Option::Some 1
    failed_effects.push Option::None
    failed_effects.push Option::Some 3
    failed = failed_effects.sequence!
    match failed
        Option::Some values => values.len!.println!
        Option::None => -1 .println!

The successful sequence prints 2 and 4. The sequence containing None prints -1, because one missing effect makes the whole Option result absent. Both sequence values and values.sequence! consume the input vector.

Choosing a Call Style

The standalone functions and receiver methods are available through the prelude:

StandaloneReceiver
fmap mapper, valuevalue.fmap mapper
ap wrapped_function, wrapped_valuewrapped_function.ap wrapped_value
bind value, callbackvalue.bind callback
foldl step, initial, valuesvalues.foldl step, initial
traverse action, valuesvalues.traverse action
traverse_m action, valuesvalues.traverse_m action
sequence valuesvalues.sequence!

The receiver APIs are ordinary blanket trait implementations on fully applied values. Implementing the corresponding constructor traits gives a custom carrier these methods automatically. They consume their receivers and preserve the callback bounds of the constructor operations, including FnMut where supported. Qualified calls such as F::Functor::fmap remain useful inside implementations or when choosing a trait explicitly.

For example, this custom carrier only implements Functor; the prelude supplies its .fmap method:

enum Parcel T
    Item T

impl Functor for Parcel
    fmap = mut mapper, value ->
        match value
            Parcel::Item item => Parcel::Item (mapper item)

increment: I64 -> I64
increment = value -> value + 1

main = !->
    parcel = Parcel::Item 4
    match parcel.fmap increment
        Parcel::Item value => value.println!

This prints 5. The standalone fmap function uses the same implementation.

Current Limits and Design Guidance

Use F _ for a unary constructor bound and Result _, E when fixing one Result parameter. Do not pass a bare binary Result where a unary constructor is required. The current stdlib intentionally gives Vec no Applicative or Monad implementation because Cartesian and zipped list semantics would make an arbitrary choice. Existing .map and .and_then methods remain useful for concrete containers; fmap and bind provide a shared vocabulary for constructor-generic code.

The examples here use constructor holes, standalone functions, and receiver methods. Explicit type-lambda syntax is not needed for these APIs.