Collections and Literals
Klyn collection literals are statically typed. The literal prefix chooses the concrete collection family: fixed-size arrays, immutable lists, dynamic lists, linked-list variants, synchronized variants, sets, or maps. The element type is inferred from the literal content unless an explicit annotation gives the compiler a stronger contract.
Importing Collections
Examples on this page that name collection interfaces or concrete collection classes assume
import klyn.collections at module level. Literal syntax itself remains available
without writing the class names explicitly.
import klyn.collections
Literal Prefixes
Prefixes are short on purpose. They encode the storage contract directly in the syntax:
f means fixed-size, i means immutable, s means
synchronized/thread-safe, l means linked-list semantics, and t means
sorted-tree map semantics. Compound prefixes
combine contracts: fs is fixed-size synchronized, and ls is linked-list
synchronized.
List-family types. These sequence literals preserve element order. Every type
implements IList<T>; resizable variants also implement List<T>.
The prefix selects fixed, immutable, dynamic, linked, or synchronized storage.
array = f:[10, 20, 30] # Array<Int> fixed-size contiguous array
arraySync = fs:[10, 20, 30] # ArraySync<Int> fixed-size thread-safe array
immutableList = i:[10, 20, 30] # ImmutableList<Int> homogeneous immutable sequence
arrayList = [10, 20, 30] # ArrayList<Int> dynamic-size list
arrayListSync = s:[10, 20, 30] # ArrayListSync<Int> dynamic-size thread-safe list
linkedList = l:[10, 20, 30] # LinkedList<Int> dynamic-size linked-list contract
linkedListSync = ls:[10, 20, 30] # LinkedListSync<Int> thread-safe linked-list contract
Sets. Set literals keep unique values without defining an iteration order;
s: selects the synchronized implementation.
hashSet = {10, 20, 30} # HashSet<Int> unique unordered values
hashSetSync = s:{10, 20, 30} # HashSetSync<Int> thread-safe unique unordered values
Maps. Plain map literals use hash-based storage, while t: keeps keys
sorted; adding s selects the corresponding synchronized implementation.
hashMap = {"a": 1, "b": 2} # HashMap<String, Int> unordered key/value storage
hashMapSync = s:{"a": 1, "b": 2} # HashMapSync<String, Int> thread-safe unordered storage
sortedMap = t:{"a": 1, "b": 2} # SortedMap<String, Int> key-sorted tree storage
sortedMapSync = ts:{"a": 1, "b": 2} # SortedMapSync<String, Int> thread-safe key-sorted storage
Empty literals need context. Use an explicit annotation or constructor when the element type cannot be inferred from values.
names as ArrayList<String> = []
ids as HashSet<UInt> = HashSet<UInt>()
index as HashMap<String, Int> = {}
A literal can use an explicit collection element type from its context. In the
following declaration, the compiler constructs one ArrayList<Object>
directly; it does not first create a string list and then convert or copy it.
Each element must be assignable to Object.
The same rule applies to set and map literals, including literals inside
conditional branches. The storage prefix is preserved. If only one branch
supplies a collection type, it also provides the context for a fresh literal
in the other branch, such as typedList if flag else [].
objects as ArrayList<Object> = ["a", "b"]
objects.add(42)
assert objects is ArrayList<Object>
unique as Set<Object> = {42} # Constructs HashSet<Object> directly.
flag = true
chosen as ArrayList<Object> = [1] if flag else [2]
strings = ["a", "b"] # No element-type context: ArrayList<String>.
# Rejected: objects = strings
Existing collections are invariant: an ArrayList<String>
is not an ArrayList<Object>. A type test without arguments,
such as strings is ArrayList, recognizes the entire family;
strings is ArrayList<Object> is false. Membership does not
depend on the elements currently stored in the collection.
Explicit generic method arguments also supply context to literals.
A Map<String, Object> does not describe the individual
type stored under each key; specify the requested return type when
an empty default cannot provide it.
config as Map<String, Object> = {"openFiles": ["main.kn"]}
files = config.get<ArrayList<String>>("openFiles", [])
missing = config.get<ArrayList<String>>("missing", [])
assert files == ["main.kn"]
assert missing is ArrayList<String>
Parameter types also provide context to fresh fixed-array literals:
passing f:[42] to an Object[] parameter
constructs an object array directly. This never converts an existing
Int[] variable into an Object[].
Fixed Arrays
Array<T> is a fixed-size contiguous array. Its size is decided at construction
time and never changes. Use it when the number of slots is known and indexed access is the
dominant operation.
values = f:[30, 10, 20]
values.sort()
assert values[0] == 10
assert values.size == 3
The explicit allocation form T[n] creates a fixed array with n
neutral-valued slots. Use T[] in type annotations when an API accepts an array whose
size is determined at runtime.
buffer as Byte[] = Byte[4096]
names as String[] = String[3]
assert buffer.size == 4096uL
assert names.size == 3uL
size is an ULong; indices are Long.
sizeof describes the array object itself, not its element buffer.
ArraySync<T> has the same fixed-size contract, but runtime operations are
synchronized for shared mutable access.
shared = fs:[1, 2, 3]
shared[0] = 10
assert shared is ArraySync<Int>
Dynamic Lists
ArrayList<T> is the default list literal. It grows dynamically and supports
indexed reads, indexed writes, appends, sorting, reversing, and membership checks.
data = [10, 20, 30]
assert data is ArrayList<Int>
assert data[0] == 10
assert data[-1] == 30
data[1] = 2000
data.add(40)
ArrayListSync<T> exposes the same typed list API with synchronized storage.
Prefer the non-synchronized version for single-threaded hot paths.
shared = s:[10, 20, 30]
shared.add(40)
assert shared is ArrayListSync<Int>
l:[...] creates a LinkedList<T>, and ls:[...] creates the
synchronized LinkedListSync<T>. They expose the same List<T>
contract as dynamic arrays. In the current runtime, linked lists use optimized native list
storage, so the literal is primarily a semantic/API choice. Prefer the type that communicates
the API contract instead of treating the prefix as a low-level storage escape hatch.
linked = l:[10, 20, 30]
linked.removeAt(1)
assert linked is LinkedList<Int>
assert linked[1] == 30
assert linked.toString() == "l:[10, 30]"
sharedLinked = ls:[10, 20, 30]
sharedLinked.add(40)
assert sharedLinked is LinkedListSync<Int>
Copying from another collection now goes through constructors, not static factories.
immutable as IList<Int> = i:[1, 2, 3]
mutable = ArrayList<Int>(immutable)
syncMutable = ArrayListSync<Int>(mutable)
linked = LinkedList<Int>(immutable)
syncLinked = LinkedListSync<Int>(linked)
List Operations
Mutable lists provide indexed insertion, bulk insertion, indexed removal, and forward or
reverse searches. Search methods return -1 when no element matches.
values = [10, 20, 10]
assert values.indexOf(10) == 0
assert values.indexOf(10, 1) == 2
assert values.lastIndexOf(10) == 2
values.add(1, 15)
values.addAll([30, 40])
values.addAll(2, (16, 17))
values.removeAt(0)
addAll is deliberately separate from add: a list can contain another
list as one element. += is the concise bulk-append form and accepts an
IList<T>, a Set<T>, or a tuple. The + operator
concatenates two values of the same concrete list family into a new list.
left = [10, 20]
right = [30, 40]
combined = left + right
left += i:[30, 40]
left += (50, 60)
assert combined == [10, 20, 30, 40]
assert left == [10, 20, 30, 40, 50, 60]
Immutable Lists
i:[...] creates an ImmutableList<T>: a homogeneous immutable sequence
exposed through IList<T>. It is the right choice when callers should read values
without mutating the collection.
Indexed replacement, slice assignment and indexed ++/--
are rejected at compilation with TypeError, rather than being compiled
into an operation that always fails at runtime. The list is structurally immutable;
mutable objects stored in it retain their own mutation operations.
values as IList<Int> = i:[10, 20, 30]
assert values[1] == 20
assert values.size == 3
Tuples
Parenthesized comma expressions build positional tuples. Tuples are fixed positional values
and may be heterogeneous. They are not List<T>.
single = (1)
pair = ("age", 42)
singletonTuple = (1,)
assert single is Int
assert singletonTuple is Tuple<Int>
A trailing comma is what makes a one-element tuple. Use i:[...] for a homogeneous
immutable sequence.
sum(tuple) accepts primitive numeric and complex positions, using their
common arithmetic type. For example, sum((10, 20.5)) is a
Double; sum((10, 30.1, 3 + 2j)) is a
Complex<Double>. The tuple is read once without a temporary collection.
An empty tuple sums to Int(0). Non-numeric positions are rejected with
TypeError, as are signed/ULong combinations with no common numeric type.
sum<Double>((10, 20)) explicitly widens the result; narrowing is rejected.
Sets
HashSet<T> stores unique values with average O(1) add, lookup, and remove.
Duplicate literal entries are ignored by the set contract.
colors = {"red", "blue", "red"}
assert colors is HashSet<String>
assert colors.size == 2
assert "red" in colors
Use s:{...} for the synchronized set variant.
sharedColors = s:{"red", "blue"}
assert sharedColors is HashSetSync<String>
An empty brace literal {} is a map, not a set. Empty sets must therefore be
constructed explicitly.
empty = HashSet<String>()
copy = HashSet<String>(["red", "blue"])
Maps
Map literals use key: value pairs and infer both key and value types.
Map<K, V> is the common interface; the prefix selects one of its concrete
implementations. The interface cannot be instantiated. The unprefixed form creates a
HashMap<K, V>.
config = {
"fullscreen": true,
"theme": "dark"
}
config["theme"] = "light"
assert config is HashMap<String, Object>
Prefix a braced literal with t: when key ordering is part of the contract. The
resulting value is a balanced-tree SortedMap<K, V>; the unprefixed form remains the
faster hash-based default.
ordered = t:{
"first": 1,
"second": 2
}
assert ordered is SortedMap<String, Int>
Prefix synchronization with s:. A synchronized hash map uses s:{...};
combine tree ordering and synchronization with ts:{...}.
shared = s:{"b": 2, "a": 1}
sharedSorted = ts:{"b": 2, "a": 1}
assert shared is HashMapSync<String, Int>
assert sharedSorted is SortedMapSync<String, Int>
assert sharedSorted.keys() == i:["a", "b"]
Use constructors to copy maps or build a map from parallel key/value lists.
keys = ["a", "b", "c"]
values = [1, 2, 3]
index = HashMap<String, Int>(keys, values)
copy = HashMap<String, Int>(index)
shared = HashMapSync<String, Int>(copy)
Index access raises KeyException when a key is absent. Use the generic
get<R>(key, defaultValue) form when a typed fallback is appropriate. The
result type R is inferred from the default value, which is particularly useful for
heterogeneous Object maps.
config as Map<String, Object> = {
"fullscreen": true,
"theme": "dark",
"retryCount": 3
}
fullscreen as Boolean = config.get("fullscreen", false)
theme as String = config.get("theme", "light")
timeout as Int = config.get("missingTimeout", 30)
explicit as String = config.get<String>("theme", "light")
assert fullscreen
assert theme == "dark"
assert timeout == 30
If the key exists, its stored value is cast to R. Asking for the wrong type is a
type error; get does not silently convert heterogeneous values.
Numeric equality is mathematical, but boxed hash keys retain their primitive type.
Therefore 1l, 1ul, and 1.0 can compare equal while
remaining three distinct keys in a HashMap<Object, V>. Within one
floating type, positive and negative zero share a key and NaN values use one canonical key.
SortedMap<K, V> uses a balanced search tree with O(log N) lookup and ordered key
traversal. HashMap<K, V> implements the common Map<K, V>
contract through a separate hash-table implementation with average O(1) lookup and no iteration-order
guarantee. Use the contract that matches the required ordering and hot-path behavior.
A for loop over a map or set starts in O(1) additional time and memory:
stopping immediately with break does not copy the collection.
The loop retains the original keys or set elements. Modifying the collection during
traversal preserves those elements in an O(n) snapshot shared by active traversals.
In for key, value in mapping, the value is read when its key is visited;
removing that key can therefore raise KeyException.
Explicit keys(), values() and items() calls
remain snapshot-producing operations.
Custom Hash Keys
Object.hash() follows Klyn's default structural equality. A class that defines a custom
operator== must override hash() from the same fields so equal objects always
produce the same hash value. The contract is shared by HashMap and HashSet.
class Identifier:
public readonly property value as Int
public Identifier(value as Int):
this.value = value
public operator==(other as Identifier) as Boolean:
return other is not null and this.value == other.value
public override hash() as Long:
return Long(this.value)
first = Identifier(42)
same = Identifier(42)
values = HashMap<Identifier, String>()
values[first] = "active"
assert same in values
assert values[same] == "active"
Never mutate a field that participates in equality or hashing while the object is stored in a hash collection. Remove the key, change it, then insert it again when such a transition is unavoidable.
Main Interfaces
| Contract | Meaning | Typical concrete types |
|---|---|---|
Collection<T> |
Iterable homogeneous collection. | ArrayList, LinkedList, HashSet, synchronized variants. |
IList<T> |
Read-only indexed homogeneous sequence. | Array, ArraySync, ImmutableList, dynamic list variants. |
List<T> |
Mutable indexed homogeneous sequence. | ArrayList, ArrayListSync, LinkedList, LinkedListSync. |
Set<T> |
Unique values. | HashSet, HashSetSync. |
Map<K, V> |
Key/value lookup contract. | SortedMap, HashMap, synchronized variants. |
Literal Type Inference
ints = [10, 20, 30] # ArrayList<Int>
doubles = [10, 20, 30.0] # ArrayList<Double>
fixed = f:[10, 20, 30] # Array<Int>
syncFixed = fs:[10, 20, 30] # ArraySync<Int>
immutable = i:[10, 20, 30] # ImmutableList<Int>
shared = s:[10, 20, 30] # ArrayListSync<Int>
linked = l:[10, 20, 30] # LinkedList<Int>
syncLinked = ls:[10, 20, 30] # LinkedListSync<Int>
setValues = {10, 20, 30} # HashSet<Int>
mixed = [10, true, "hello"] # ArrayList<Object>
mapping = {"a": 1, "b": 2} # HashMap<String, Int>
Unpacking
a, b, c = [10, 100, 1000]
x, y, z = (1, 2, 3)
Unpacking works with list-like values and tuples when the number of values matches the number of targets.
Slices
Fixed arrays and dynamic array lists support
[start:stop:step]. Omitted bounds and negative indexes follow the same rules as
string slices, and the stop index is exclusive. A zero step is invalid.
data = [10, 20, 30, 40, 50]
assert data[1:4] == [20, 30, 40]
assert data[:3] == [10, 20, 30]
assert data[::2] == [10, 30, 50]
assert data[::-1] == [50, 40, 30, 20, 10]
Slice assignment replaces the selected range. Dynamic lists accept compatible list, fixed array, immutable-list, synchronized-list, or tuple values. Fixed arrays keep their fixed-size contract, so the replacement must fit the selected slots. The receiver, explicit bounds, step and replacement are evaluated once, in that order. An exception in a bound prevents evaluation of the remaining operands and does not perform the slice write.
Dynamic-list assignments capture the replacement values before writing, including when
the source is the target itself. An incompatible element is rejected without modifying
the target. Compatible primitive values keep their compact storage when a slice grows or
shrinks the list. For contiguous lists, reading or replacing k elements without
resizing takes O(k) time, independent of the total list size. Resizing additionally
moves the suffix and, if necessary, reallocates the backing buffer.
data[1:4] = s:[200, 300, 400]
assert data == [10, 200, 300, 400, 50]
fixed = f:[10, 20, 30, 40]
fixed[1:3] = [200, 300]
assert fixed == f:[10, 200, 300, 40]
Comprehensions
Comprehensions build a new collection by iterating over an existing collection and evaluating an expression for each element. The result is still statically typed: the compiler infers the output element type from the generated expression, not from a dynamic runtime scan.
source = [10, 20, 30]
doubled = [x * 2 for x in source] # ArrayList<Int>
assert doubled == [20, 40, 60]
An iteration name reuses an already visible variable or parameter; it does not redeclare or shadow that variable. Each item is assigned before the filter and projection run, using the normal type and read-only checks. After construction the variable holds the last iterated value, even if the filter rejected that item. An empty source leaves it unchanged. A name that was not visible belongs only to the comprehension and does not escape it. This rule applies independently to each name in tuple destructuring, including nested comprehensions.
x = 100
y = 200
pairs = [(1, 2), (3, 4)]
result = [x + y for x, y in pairs]
assert result == [3, 7]
assert x == 3
assert y == 4
Add an if clause when only some elements should be kept. The filter is evaluated
before appending the transformed value to the result collection.
large = [x * 2 for x in source if x > 15]
assert large == [40, 60]
The prefix before the comprehension controls the concrete result type, exactly like it does for plain literals.
dynamic = [x + 1 for x in source] # ArrayList<Int>
shared = s:[x + 1 for x in source] # ArrayListSync<Int>
linked = l:[x + 1 for x in source] # LinkedList<Int>
syncLinked = ls:[x + 1 for x in source] # LinkedListSync<Int>
immutable = i:[x + 1 for x in source] # ImmutableList<Int>
fixed = f:[x + 1 for x in source] # Array<Int>
syncFixed = fs:[x + 1 for x in source] # ArraySync<Int>
Use fixed-array comprehensions when the result is read mostly by index and should not grow
after construction. Use linked-list comprehensions when an API should explicitly expose
LinkedList<T> or LinkedListSync<T>. Use synchronized comprehensions only
when the resulting collection is shared across threads; the unsynchronized variants remain
the better default for hot paths.
names = ["Ada", "Grace", "Linus"]
upperNames = f:[name.toUpper() for name in names]
assert upperNames is Array<String>
assert upperNames[0] == "ADA"
Comprehensions can also change the element type. In the next example, the source is a list of strings while the result is a list of integers.
words = ["Klyn", "is", "fast"]
sizes = [word.size for word in words] # ArrayList<UInt>
assert sizes[0] == 4u
Map comprehensions use keyExpr: valueExpr. Their prefix follows the same rules:
no prefix produces HashMap, s: produces HashMapSync,
t: produces SortedMap, and ts: produces
SortedMapSync. When the source is a map, iteration may bind both the key and the
value with for key, value in map.
scores = {"alice": 12, "bob": 7, "chloe": 18}
bonus = {name.toUpper(): score + 1 for name, score in scores}
assert bonus["ALICE"] == 13
passing = {name: score for name, score in scores if score >= 10}
orderedPassing = t:{name: score for name, score in scores if score >= 10}
sharedPassing = s:{name: score for name, score in scores if score >= 10}
sharedOrderedPassing = ts:{name: score for name, score in scores if score >= 10}
assert "bob" not in passing
assert orderedPassing is SortedMap<String, Int>
assert sharedPassing is HashMapSync<String, Int>
assert sharedOrderedPassing is SortedMapSync<String, Int>
Set comprehensions use braces without a key/value separator. The unprefixed form produces a
HashSet; s: produces a HashSetSync. Duplicates are removed while
values are generated, without constructing an intermediate list. The t: and
ts: prefixes remain reserved for sorted map comprehensions and therefore require a
key: value projection.
raw = [1, 2, 2, 3, 3, 3]
uniqueDoubled = {x * 2 for x in raw} # HashSet<Int>
sharedLarge = s:{x * 2 for x in raw if x > 1} # HashSetSync<Int>
assert uniqueDoubled.size == 3uL
assert 2 in uniqueDoubled
assert 4 in sharedLarge
Iteration Helpers
values = [10, 20, 30]
for value in values:
print(value)
for key in config.keys():
print(key)
for value in config.values():
print(value)
for entry in config.items():
print(entry)
Direct map iteration yields keys. Use helper views when you want values or key/value tuples.