Skip to content

Work with collections ​

This recipe follows storage through a slice, an independent fixed-array copy, a pointer view, an allocated destination slice, and a map. It also exercises the collection built-ins that mutate or inspect those values.

Generic indexing and slicing ​

Since v0.4.1, a common underlying collection shape allows direct indexing and slicing inside generic functions. Returning a slice of S retains S, including named slice types; writing through that slice updates the original backing array. A full slice can restrict its capacity:

ts
import go { Println } from "fmt"

constraint Slice<E> = ~E[]

function first<E, S extends Slice<E>>(values: S): E {
  return values[0]
}

function window<E, S extends Slice<E>>(values: S): S {
  return values[1:3:3]
}

type Numbers = distinct int[]

function main(): void {
  const values = Numbers([1, 2, 3, 4])
  const view: Numbers = window(values)
  view[0] = 9
  Println(first(values), values[1], len(view), cap(view))
}

The same rules cover fixed arrays, array pointers, strings, and map indexing with an optional presence result. Arrays retain value-copy semantics; array pointers share their array. Class/interface elements retain their source types and required null checks. A map slot is writable but not addressable, and a string byte cannot be assigned:

ts
constraint Text = ~string

function invalid<S extends Text>(value: S): void {
  value[0] = 65
}

Indexing also accepts unions of arrays (including different lengths), slices, and array pointers with identical element types. String/byte-slice unions support read-only byte indexing and two-index slicing that retains the input type. Array/slice unions cannot be sliced under the Go 1.23 baseline:

ts
import go { Println } from "fmt"

constraint Sequence<E> = ~E[] | ~[2]E | ~*[3]E
constraint Text = ~bstring | ~byte[]

function head<E, S extends Sequence<E>>(values: S): E {
  return values[0]
}

function suffix<T extends Text>(value: T): T {
  return value[1:]
}

function main(): void {
  const pair: [2]int = [7, 8]
  const values: byte[] = [65, 66, 67]
  const rest: byte[] = suffix(values)
  rest[0] = 90
  Println(head<int, [2]int>(pair), suffix(b"ABC"), bstring(values))
}

A constraint containing strings cannot support writes or three-index slicing:

ts
constraint Text = ~string | ~byte[]

function limit<T extends Text>(value: T): T {
  return value[0:1:1]
}

Dynamic index/slice bounds retain Go panics; constant bounds are checked against every array alternative. Unrepresentable numeric map keys and mismatched nullable element contracts are checked before emission. Specify generic arguments where the mixed constraint cannot infer the element type.

Project tree ​

text
collections/
└── main.km

Source ​

ts
import go fmt from "fmt";

function total(values: int[]): int {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum;
}

function main(): void {
  const values = [2, 4, 6];
  const suffix = [10, 12];
  const extended = append(values, suffix...);
  const fixed: [3]int = copyArray[[3]int](values);
  const viewed: *[3]int = viewArray[[3]int](values);
  (*viewed)[1] = 40;

  const destination = makeSlice[int](3, 5);
  const copied = copy(destination, values);
  const copiedMiddle = destination[1];
  clear(destination);

  const encoded = makeSlice[byte](5);
  const encodedCount = copy(encoded, "hello");

  const scores = makeMap[string, int]();
  scores["hello"] = total(extended);
  const [score, present] = scores["hello"];
  scores["temporary"] = 1;
  delete(scores, "temporary");
  const [_, temporaryPresent] = scores["temporary"];

  fmt.Println(
    len(extended), cap(destination), fixed[1], values[1],
    copied, copiedMiddle, destination[1], score, present, temporaryPresent,
    min(2, 8, 4), max(2, 8, 4),
  );
  fmt.Println(bstring(encoded), encodedCount);
}

Run ​

sh
keika check main.km
keika run main.km

Expected output:

text
5 5 4 40 3 40 0 34 true false 2 8
hello 5

What happens ​

  1. values is inferred as an int[] slice.
  2. append(values, suffix...) expands one compatible slice and returns the result; it does not silently reassign values.
  3. copyArray[[3]int](values) creates an independent fixed-array value.
  4. viewArray[[3]int](values) creates a pointer view. Writing (*viewed)[1] therefore changes values[1], while fixed[1] remains 4.
  5. makeSlice[int](3, 5) separates length from capacity. copy reports three copied elements.
  6. clear(destination) zeroes its elements without changing its length or capacity.
  7. copy(encoded, "hello") copies five raw string bytes into a byte[] and reports the copied count.
  8. Range binds each slice value and evaluates the source once.
  9. [score, present] exposes Go's comma-ok map lookup; delete makes the temporary key absent again.
  10. min and max inspect compatible ordered values without changing their type.

Missing and nil maps return the value type's zero value plus false. Map iteration order is unspecified; this example never depends on it.

Both array-conversion forms panic if the source is shorter than N. The view also carries ordinary pointer aliasing: keep it only while shared mutation is intentional and the backing storage remains valid.

Generic clearing and deletion ​

Since v0.4.0, clear accepts a type parameter whose possible types are all slices or maps. delete requires map types with the same key type, but their value types may differ. Both are available in generic class methods too.

ts
import go { Println } from "fmt"

constraint Resettable = ~int[] | ~Map<string, int>
constraint Lookup = ~Map<string, int> | ~Map<string, string>

function reset<T extends Resettable>(value: T): void {
  clear(value)
}

class Cleaner {
  public function remove<T extends Lookup>(value: T, key: string): void {
    delete(value, key)
  }
}

function main(): void {
  const values = [1, 2, 3, 4]
  const part = values[1:3]
  reset(part)
  Println(values, len(part), cap(part))

  const counts = makeMap<string, int>()
  counts["temporary"] = 1
  const labels = makeMap<string, string>()
  labels["temporary"] = "draft"
  const cleaner = new Cleaner()
  cleaner.remove(counts, "temporary")
  cleaner.remove(labels, "temporary")
  reset(counts)
  Println(len(counts), len(labels))
}

Expected output:

text
[1 0 0 4] 2 3
0 0

Clearing a slice changes its shared backing elements without changing length or capacity; elements beyond its length remain untouched. Clearing a map removes every entry, including NaN keys. Nil maps/slices are safe. Deletion checks the key's type, source nullability and numeric constant range at compile time.

Generic append and copy ​

Since v0.4.0, a slice constraint also supports append and copy. append returns the destination's original type, including a named slice type. copy returns the number of elements copied and preserves the destination's length. Both work inside generic classes and methods.

ts
import go { Println } from "fmt"

constraint Slice<E> = ~E[]
type Numbers = distinct int[]

function extend<E, S extends Slice<E>>(values: S, extra: E[]): S {
  return append(values, extra...)
}

class Copier<E> {
  public function copy<D extends Slice<E>, S extends Slice<E>>(destination: D, source: S): int {
    return copy(destination, source)
  }
}

class Base {
  constructor(public value: int) {}
  public virtual function read(): int { return this.value }
}

class Twice extends Base {
  constructor(value: int) { super(value) }
  public override function read(): int { return this.value * 2 }
}

function addDerived<S extends Slice<Base>>(values: S): S {
  return append(values, new Twice(6))
}

function main(): void {
  const original = Numbers([1])
  const extended = extend(original, [2, 3])
  Println(len(original), len(extended), extended)

  const destination = makeSlice<int>(2)
  const copied = new Copier<int>().copy(destination, extended)
  Println(copied, destination)

  const objects: Base[] = []
  const result = addDerived(objects)
  Println(result[0].read())
}

Expected output:

text
1 3 [1 2 3]
2 [1 2]
12

Source and destination elements must match for copying or spread-append, including their nullability and generic arguments. Adding one derived-class value to a base-class slice is allowed and preserves virtual dispatch; copying a whole derived-class slice into a base-class slice is not. A byte-slice constraint also supports appending or copying bytes from a string-constrained source.

See Types and data and Type-system reference.

Kinmokusei is a pre-1.0 project. Documentation describes implemented behavior.