Typing and Constants
Klyn is statically typed from top to bottom. Type inference keeps declarations compact, but the inferred type is still a real compile-time type. This page explains the practical rules that matter when you declare variables, annotate APIs, and rely on constants.
Klyn Is Always Typed
count = 10 # inferred as Int
ratio = 0.5 # inferred as Double
name = "Klyn" # inferred as String
count = 12 # valid
count = "twelve" # invalid
Inference is convenient syntax, not dynamic typing. The compiler remembers the type and checks every later assignment against it.
This also fixes generic arguments and tuple arity: a variable inferred as
ArrayList<Int> cannot later become an
ArrayList<String>, and Tuple<Int> cannot become
Tuple<Int, String>. Use distinct variables for different types.
An inferred variable does not carry a dynamic type tag that changes with each assignment. The compiler resolves its type once, generates code for that type, and rejects incompatible uses before execution.
Explicit Type Annotations
import klyn.collections
count as Int = 10
price as Double = 19.99
names as ArrayList<String> = ["Ada", "Grace"]
Use explicit annotations when the type is part of the design, when a public API must stay self-documenting, or when you want a specific numeric type instead of the default literal type.
In local implementation code, inference is usually the clearest choice. Public properties, method parameters, return values, collection boundaries, and persisted models should normally expose their types explicitly.
Declaration Before Use
A variable must be declared before its first use. Klyn does not create an implicit global with a neutral value merely because a declaration appears later in the file. This keeps name resolution deterministic and catches initialization-order mistakes at compile time.
limit as Int = 10
print(limit) # valid
print(otherLimit) # invalid: otherLimit is not declared yet
otherLimit = 20
Assignment to a Visible Variable
An assignment without as reuses the nearest visible variable.
This also applies inside a function or method: assigning to an existing
module-level variable modifies that variable, without a global
declaration. Only a previously unknown name creates an inferred local variable.
Parameters and local variables take precedence over module-level variables.
total as Int = 0
def addToTotal(amount as Int):
total = total + amount # updates the module variable
doubled = amount * 2 # local to this function
addToTotal(3)
assert total == 3
Assignment does not change the variable's declared type. If a visible variable
is a Double, assigning a Long to it performs the permitted
numeric conversion; it does not turn the variable into a Long.
Use a distinct name when you need a separate inferred local value.
Numeric Types
| Type | Width | Range or precision | Typical literal |
|---|---|---|---|
SByte |
8 bits | -128 to 127 | SByte(10) |
Byte |
8 bits | 0 to 255 | Byte(10) |
Short |
16 bits | -32,768 to 32,767 | Short(10) |
UShort |
16 bits | 0 to 65,535 | UShort(10) |
Int |
32 bits | -2,147,483,648 to 2,147,483,647 | 10 |
UInt |
32 bits | 0 to 4,294,967,295 | 10u |
Long |
64 bits | -263 to 263-1 | 10l |
ULong |
64 bits | 0 to 264-1 | 10ul |
Float16 |
16 bits | IEEE 754 binary16 (half precision) | 0.5h |
Float |
32 bits | IEEE 754 single precision | 0.5f |
Double |
64 bits | IEEE 754 double precision | 0.5 |
Int and Double are the default integer and floating-point types.
Choose another width when the domain, binary format, native ABI, or memory footprint requires it.
The concrete primitive types Byte, SByte, Short,
UShort, Int, UInt, Long,
ULong, Float16, Float, Double,
Boolean, Char, and CodeUnit are
final: they cannot be subclassed. Their abstract numeric bases remain
available for generic constraints. For example, class Counter extends Int:
is rejected with SyntaxError; use a class containing an Int
property when a value needs additional state.
Integer arithmetic uses a common type that covers the complete ranges of both operands,
independently of their values. Mixed signed and unsigned operands use a sufficiently wide
signed type: SByte + Byte produces Short,
Short + UShort produces Int, and
Int + UInt or Long + UInt produces Long.
No signed integer type covers the complete ULong range, so mixing a signed
integer with ULong requires an explicit conversion. This rule applies to
+, -, *, //, %, and
**. True division / produces a floating-point result;
comparisons keep their exact signed/unsigned comparison rules.
assert (-5 // 2u) is Long
assert -5 // 2u == -3l
assert -5 % 3u == 1l
assert -5l // Long(2ul) == -3l # Explicit, range-checked conversion.
count = 3ul + 1ul # Keep unsigned arithmetic unsigned.
Negating Signed Integers
Unary - preserves the operand's type and width for
SByte, Short, Int, and Long;
it does not implicitly widen small integers. These types use two's complement:
their negative range contains one more value than their positive range.
Consequently, negating MIN_VALUE wraps at that width and produces
MIN_VALUE again, without raising an exception. This behavior is
defined by Klyn and is identical for a constant and a value stored in a variable.
To obtain the positive mathematical opposite, explicitly convert to a wider
signed type before negating. Widening the result afterwards is too late.
No wider built-in signed integer can represent the opposite of Long.MIN_VALUE.
minimum = Short.MIN_VALUE
sameWidth = -minimum # Short: -32768, fixed-width wraparound.
wider = -Int(minimum) # Int: 32768, conversion before negation.
Assignments must respect Klyn's static numeric conversion rules. Use an
explicit conversion such as Float(value) or UInt(value) when the
target type is not implicitly assignable; an explicit conversion still checks its range.
A permitted integer-to-floating conversion is not necessarily exact: a
Double cannot represent every Long value without rounding.
Numeric Literal Forms
decimal = 1_000_000
binary = 0b1010
octal = 0o755
hexadecimal = 0xFF
unsigned = 42u
wide = 42l
wideUnsigned = 42ul
halfPrecision = 3.14h
singlePrecision = 3.14f
doublePrecision = 3.14
Underscores improve readability and do not affect the value. Integer bases may use the same unsigned and width suffixes as decimal integers. Suffix letters are case-insensitive, but lowercase forms are easier to read consistently.
An unsuffixed integer literal can take the expected integer type when its value fits that type. This applies to assignments, arguments, return values, typed collection elements, and conditional expressions whose two result branches are representable literals. Out-of-range literals are rejected at compilation. This does not permit implicit narrowing of an integer variable; use an explicit conversion for that case.
value as Byte = 10
flag = false
small as Byte = 10 if flag else 20
def minimum() as Short:
return -32768
def accept(value as SByte) as SByte:
return value
assert accept(42) == SByte(42)
# invalid as Byte = 256 # TypeError: the literal is out of range.
NaN and Infinity
Klyn provides six special floating-point words. They are literals with fixed compile-time types, not variables and not function calls.
| Word | Type | Meaning |
|---|---|---|
nanh |
Float16 |
Half-precision quiet not-a-number value. |
infh |
Float16 |
Half-precision positive infinity; write -infh for negative infinity. |
nan |
Double |
Quiet not-a-number value. |
nanf |
Float |
Single-precision quiet not-a-number value. |
inf |
Double |
Positive infinity; write -inf for negative infinity. |
inff |
Float |
Single-precision positive infinity; write -inff for negative infinity. |
assert type(nan) == Double
assert type(nanf) == Float
assert type(inf) == Double
assert type(inff) == Float
assert type(0.5h) == Float16
assert type(nanh) == Float16
assert type(infh) == Float16
assert nan != nan
assert not (nan < 0.0)
assert not (nan >= 0.0)
assert inf > 1.0
assert -inff < 0.0f
# Readable IEEE identity predicates preserve the literal precision.
assert nan is nan
assert nanf is nanf
assert nanh is nanh
assert -infh is -infh
assert inf is inf
assert -inff is -inff
assert 42.0 is not nan
NaN is unordered: it is unequal to every value, including itself, and every ordered comparison
involving NaN returns false. Use is nan, is nanf, or is nanh when testing its
category rather than trying equality. The corresponding is inf,
is -inf, is inff, is -inff, is infh, and is -infh predicates distinguish the
sign and precision of infinity. Runtime division or remainder by zero still raises
ZeroDivisionException; use these words when an infinite or NaN value is explicitly
part of the model.
Import klyn.math and use Math.isClose() when two floating-point results
should be compared with relative or absolute tolerances. Its defaults are
rel_tol=1e-9 and abs_tol=0.0; negative tolerances raise
ValueException. IEEE special values keep the rules above.
import klyn.math
assert Math.isClose(0.1 + 0.2, 0.3)
assert Math.isClose(0.0, 1e-12, abs_tol=1e-9)
assert not Math.isClose(nan, nan)
assert Math.isClose(inf, inf)
Half Precision
Float16 stores an IEEE 754 binary16 value in two bytes, including in
arrays and NDArray<Float16>. Its largest finite value is 65504,
its smallest positive normal value is 2-14, and its smallest positive
subnormal is 2-24. It has 11 significant binary digits, not the precision
of Float. Rounding uses nearest, ties to even. Widening to
Float or Double is exact; narrowing is explicit.
value = 123.456h
assert sizeof(Float16) == 2
assert 1.00048828125h == 1h
wider as Float = value
narrower = Float16(0.1)
assert narrower == 0.1h
For FP16 model storage, optimizers and CPU/GPU execution, continue with Deep Learning.
Displaying Floating-Point Values
Float16, Float and Double use the shortest decimal digits that
convert back to the same value of the original type. Display does not arbitrarily
round values to six digits or widen a Float to expose extra digits.
The same rules apply to toString(), String(value),
printing, collection elements and interpolation without a format specifier.
Explicit formatting keeps its existing precision and presentation rules.
Decimal notation is used when the decimal exponent is between -4 and 6 inclusive
(magnitudes from 0.0001 up to, but not including, 10000000).
Otherwise, scientific notation uses a lowercase e, an explicit exponent
sign and no leading exponent zeros. The exponent is determined from the shortest
decimal representation, so 0.0001f still displays as 0.0001.
Integral mantissas retain .0; zero is 0.0 or -0.0.
Special values display as nan, inf and -inf,
with no f suffix for Float.
assert (3.0).toString() == "3.0"
assert (0.1f).toString() == "0.1"
assert (0.1 + 0.2).toString() == "0.30000000000000004"
assert (0.0001).toString() == "0.0001"
assert (0.00001).toString() == "1.0e-5"
assert (10_000_000.0).toString() == "1.0e+7"
value = 3.1415926
assert f"{value}" == "3.1415926"
assert f"{value:.2f}" == "3.14" # Explicit formatting controls rounding.
Conversions, Equality, and Hash Keys
small as Float = Float(3.5)
count as UInt = UInt(42)
assert 1l == 1ul
assert 1l == 1.0
assert 0.0 == -0.0
An integer-to-integer cast with as keeps the low bits of the target width,
interpreting its sign according to the target type. This applies immediately, even when
the cast is compared or passed as an argument without storing it. In contrast,
constructors such as Byte(value) check the representable range and reject
an out-of-range value. Floating-to-integer conversions also reject non-finite or
out-of-range values. An unknown cast target is a compile-time TypeError.
source = 256
assert (source as Byte) == Byte(0)
wide = 2147483648l
assert (wide as Int) == Int.MIN_VALUE
# Byte(source) raises ValueException: 256 is outside Byte's range.
Numeric comparisons are mathematical across primitive numeric types and preserve full integer precision. This does not make differently typed boxed values the same hash key. Maps and sets retain the primitive key type, following the strict key semantics commonly expected from a statically typed runtime.
import klyn.collections
values = HashMap<Object, String>()
values[1l] = "signed"
values[1ul] = "unsigned"
assert values.size == 2
assert values[1l] == "signed"
assert values[1ul] == "unsigned"
const and readonly
const declares a binding that cannot be assigned again. It does not imply that
every initializer is evaluated by the compiler, nor that every referenced object becomes
deeply immutable. readonly instead describes instance state initialized once or a
property that callers can read but cannot set.
These guarantees depend on the declaration modifiers, not on the spelling of an identifier.
An uppercase name is not automatically constant, and a lowercase const
binding has exactly the same protection as an uppercase one.
| Contract | const |
readonly |
|---|---|---|
| Primary guarantee | The declared binding cannot be reassigned | The field is initialized once, or the property has no writable setter |
| Initialization | Mandatory; any type-compatible expression is accepted | At declaration or during instance construction |
| Class member storage | Implicitly static | Normally per instance; a static readonly property may expose a computed class value |
| Reference types | The reference is fixed; the referenced object may remain mutable | A returned reference may still designate a mutable object |
const PI = 3.141592653589793
const APP_NAME as String = "Klyn"
class ApplicationInfo:
public const BASE as Int = 40
public const ANSWER as Int = BASE + 2
def demo():
const RETRY_COUNT as Int = 3
print(ApplicationInfo.ANSWER, RETRY_COUNT)
def describe(readonly value as Int) as String:
# value = 0 # Compile-time error: the parameter is fixed.
return String(value)
A const declaration requires an initializer; a readonly parameter receives
its value from the call and cannot be rebound inside the function (see
Parameter Passing and Contracts). Class constants belong to the type itself, so
ApplicationInfo.ANSWER is available without an instance. Do not write
static const: static would be redundant and the compiler rejects it.
Constant bindings cannot be reassigned, incremented, decremented, or modified through compound
assignment.
import klyn.collections
import klyn.time
class Dates:
public const BIRTH_DAY = DateTime(1973, 8, 26)
public const LABELS = ArrayList<String>()
assert Dates.BIRTH_DAY.year == 1973
Dates.LABELS.add("birthday") # Allowed: the reference is constant.
# Dates.BIRTH_DAY = DateTime(2000, 1, 1) # Compile-time error.
# Dates.LABELS = ArrayList<String>() # Compile-time error.
For a ValueType such as Timestamp, the constant denotes the value itself.
DateTime is a reference type and accepts null to represent an absent date.
For a reference type, const has final-reference semantics: the reference cannot change,
but the object's public mutation operations remain available. String is immutable, independently of const.
MutableString supports content mutation: a const MutableString
fixes its reference, not its contents.
"text" creates an immutable String.
m"text" creates a MutableString. The types do not
inherit from each other. Convert explicitly with String(value),
value.toString(), or MutableString(value).
text = "Klyn"
alias = text
text += "!" # Rebinds text; alias remains "Klyn".
editable = m"Klyn"
snapshot = editable.toString()
sameObject = editable
editable[0] = 'k'
assert sameObject == "klyn"
assert snapshot == "Klyn" # Also guaranteed for values read through reflection.
const LABEL = "Ready" # Immutable text and a fixed reference.
const BUFFER = m"Ready" # Fixed reference, editable contents.
BUFFER[0] = 'r'
Index and slice assignment, lower(), upper(),
capitalize(), title(), trim(),
ltrim(), rtrim(), and removeAccents()
are available only on MutableString.
Read-only transformations such as toUpper() and
replace() return String snapshots.
split() and lines() return lists of immutable strings.
toString() shares the immutable backing text without copying it.
Editing a MutableString replaces that backing value, so snapshots and
copies remain unchanged. Index and slice replacement copy text; use
StringBuilder when assembling a large result through many appends.
A mutable value must not be edited while it is used as a map or set key.
StringBuilder(size) and StringBuilderSync(size)
reserve UTF-16 CodeUnit storage, not characters. This reservation
does not change the logical length: size and length()
count Unicode scalars. The constructor argument and logical lengths use
ULong; scalar positions and indices use Long.
The buffer grows geometrically as needed;
clear() retains its capacity for reuse. A toString()
result is an immutable snapshot, unaffected by later builder edits.
builder = StringBuilder(64ul) # Reserve 64 UTF-16 code units.
assert builder.size == 0ul
builder.append('A').append('\U0001F600')
assert builder.size == 2ul # Two Char values, stored in three CodeUnit values.
text = builder.toString()
builder.clear()
assert text == "A\U0001F600"
Combine m with f for interpolation or
r for raw text: fm"Hello {name}",
rm"C:\folder", and frm"{name}\n".
Flags may appear in any order, once each, immediately before double quotes.
Triple-double-quoted strings support the same prefixes.
if remains a conditional keyword; i"..." is not a string literal.
Both types count Unicode scalars for size, indexing and slicing.
Negative indices count from the end: "Message"[-7] == 'M'.
Arrays returned by codePoints or codeUnits are independent copies.
class Constants:
public const ANSWER = 40 + 2
public const STARTED_AT = DateTime.now
The compiler folds ANSWER because its initializer is a constant expression.
STARTED_AT is nevertheless legal: it is initialized exactly once at runtime and stored
as a class constant. Whether an initializer can be folded is an optimization decision and does not
change the source-level meaning of const.
import klyn.collections
class Project:
private readonly _identifier as Int
public readonly property files as ArrayList<String>
public Project(identifier as Int):
this._identifier = identifier
this.files = []
project = Project(7)
project.files.add("main.kn") # Allowed: the property is shallow.
# project.files = [] # Compile-time error: no setter.
Keep readonly for parameters, instance fields that are assigned during construction, and
getter-only properties. Use const for a local binding or a class-owned
value whose binding must never change.
The complementary writeonly property contract exposes a setter without a getter.
It is documented with property accessor rules in
Attributes and Properties.
Null References and notnull Contracts
Reference values (classes, interfaces, strings, arrays, and collections) may hold null
by default, whereas primitive and ValueType values are never null. Use
is null and is not null for explicit null checks, and add the
notnull modifier when a reference variable or parameter must never hold null.
A ValueType declaration without an initializer must be
definitely assigned before its value is read, on every path reaching
that read. Instance fields and automatic properties of these types
must be initialized by a field initializer or on every normal
constructor exit. No constructor is implicitly called to manufacture
a default value. Invalid reads fail with TypeError;
casting a null value known only at runtime to a ValueType
raises NullException.
Dereferencing a reference that is statically known to be null
fails compilation with TypeError. This applies to instance
methods, properties, fields, indexing and slicing. When nullability is
unknown, the access is checked at runtime and raises NullException
on a null reference. Passing null to a nullable parameter remains legal.
Use is null and is not null for null guards:
unlike == and !=, these identity tests never
invoke a user-defined equality operator or its parameter contracts.
message as String = null
if message is null:
print("no message")
else:
print(message)
# Local non-null reference binding (explicit type or inferred)
notnull currentLabel as String = "ready"
notnull activeTag = "v1"
currentLabel = "running" # OK: non-null reassignment
# currentLabel = null # Compile-time TypeError.
def pickLabel(flag as Boolean) as String:
return "done" if flag else null
# Checked at runtime when static analysis cannot prove non-nullability:
try:
currentLabel = pickLabel(false)
catch err as NullException:
print("Rejected null reassignment")
A local notnull variable must be initialized when declared and remains mutable, but every
assignment to it is guarded:
-
Compile-time check: initializing or reassigning with a literal
null(or an expression statically proven to benull) fails compilation withTypeError. -
Runtime check: when an expression's nullability depends on runtime control flow,
the compiler inserts a guard that raises
NullExceptionbefore the variable can be bound tonull. -
Reference types only: applying
notnullto a primitive orValueType(such asIntorTimestamp), or combiningconstwithnotnull, is rejected at compile time.
def lengthOf(notnull value as String) as ULong:
return value.size
def joinAll(notnull parts... as String) as String:
return ",".join(parts)
On function, method, constructor, and lambda parameters, notnull enforces the same
two-level guarantee (compile-time TypeError when provably null, runtime
NullException otherwise) at the call boundary and on any reassignment inside the body.
On variadic parameters (notnull parts... as String), each passed element is checked
individually.
Library APIs use these contracts for required file-reader/writer paths, text-search patterns and callbacks, JSON documents and destination writers, scaler data, model inputs and targets, and required NDArray shapes and tensor operands. JSON values may still be null; optional encodings, search settings, reduction axes, masks, result buffers and histogram weights retain their documented null defaults. A weighted-mean or weighted-bin-count overload, however, requires a non-null weight tensor.
notnull also combines with readonly, ref, and out
parameters—for example, notnull out result as String lets the caller pass an
uninitialized or null variable while guaranteeing that the function assigns a non-null
reference before returning. See
Non-Null Parameter Contracts in the Functions chapter
for complete examples.
Function and Lambda Types
A lambda with parameters encloses them in parentheses, even with just one parameter:
lambda(value as Int): expression.
With no parameters, lambda: expression is equivalent to
lambda(): expression. The form lambda value: expression
is a syntax error. Whitespace before the opening parenthesis is optional.
square as (Int) -> Int = lambda(value as Int) as Int: value * value
power = lambda(value as Double, exponent as Double) as Double:
return value ** exponent
assert type(square) == (Int) -> Int
assert type(power) == (Double, Double) -> Double
A function type lists its parameter types before -> and its return type after it.
Klyn can infer the variable type from a typed lambda, while an explicit function annotation is
useful for callbacks, fields, collection elements, and public method parameters. Structural
function types are the normal callable types in statically typed application code: unlike a
return-only reflective abstraction, they preserve both arity and every parameter type.
Lambda parameters may omit annotations when an expected function signature supplies their
types. The return type is inferred from the body using the same rules as ordinary expressions.
If the compiler cannot determine a complete static signature, compilation fails with
TypeError: parameters
never become dynamically typed and do not default to Int.
increment as (Int) -> Int = lambda(value): value + 1
assert increment(41) == 42
square = lambda(value as Double): value * value
assert type(square) == (Double) -> Double
answer = lambda(): 42
assert answer() == 42
# Rejected: no annotation or expected signature determines the parameter type.
# ambiguous = lambda(value): value + 1
Lambda parameters may declare trailing default values. Each omitted argument is evaluated
when the lambda is called, in the lambda's captured scope; earlier parameters are available,
but the current parameter and later parameters are not. Supplying an argument, including
null for a reference type, does not evaluate its default expression.
scale = lambda(value as Int = 10): value * 2
assert scale() == 20
assert scale(3) == 6
scale = lambda(value as Int = 20): value * 3
assert scale() == 60
callback as (Int) -> Int = scale
assert callback(3) == 9
# callback() is rejected: (Int) -> Int requires one argument.
Optional arguments are available only when known statically for the lambda value.
A structural function type records by-value parameter types and the result, not default values
or ref / out passing modes:
assigning or passing a lambda as (Int) -> Int keeps its full-arity by-value contract,
and functions with ref or out parameters cannot be converted to a
(T) -> R function value.
Defaults belong to the current lambda, so reassignment does not reuse a previous
lambda's defaults. A parameter type can also be inferred from a statically typed default,
such as lambda(value = 10): value * 2.
def calculate(
left as Int,
right as Int,
operation as (Int, Int) -> Int
) as Int:
return operation(left, right)
combine = lambda(a as Int, b as Int) as Int: a + b
assert calculate(20, 22, combine) == 42
Use () -> Void for an action without parameters and
(A, B) -> R for a callback with parameters. The
klyn.reflection.Invocable<R> interface is reserved for advanced reflective calls
whose arguments are discovered at runtime and packed in an Object[]. Klyn does not
implicitly convert a structural function to Invocable<R>, because doing so would
discard its statically checked parameter signature.
CodeUnit, Char, and Unicode
Klyn separates UTF-16 representation from Unicode text semantics. A CodeUnit is
exactly one 16-bit UTF-16 code unit. A Char is exactly one 32-bit Unicode scalar
value and can therefore represent a supplementary character without splitting it into a
surrogate pair.
| Type | Literal | Storage | Use |
|---|---|---|---|
CodeUnit |
`K`, `\u03C0` |
16-bit UTF-16 code unit | UTF-16 buffers, SAX APIs, native interoperability |
Char |
'K', '😀', '\U0001F600' |
32-bit Unicode scalar | String indexing, text algorithms, Unicode characters |
letter as CodeUnit = `K`
letterScalar as Char = letter
smile as Char = '😀'
assert letterScalar == 'K'
assert smile.toInt() == 0x1F600
assert smile.utf16SequenceLength == 2u
# Narrowing is explicit and only valid for a Char that fits in one UTF-16 unit.
assert CodeUnit(letterScalar) == letter
A supplementary character such as 😀 cannot be stored in one CodeUnit. UTF-16
represents it with a high and low surrogate, while Char stores the complete scalar.
Klyn converts CodeUnit to Char implicitly, but validates the code unit:
an isolated surrogate raises ValueException. Narrowing a supplementary
Char to CodeUnit also raises ValueException.
Strings Are Scalar Text, Not Byte Buffers
text = "A😀πB"
bytes as Byte[] = f:[Byte(75), Byte(108), Byte(121), Byte(110), Byte(0), Byte(1)]
assert text.size == 4u
assert text[1u] == '😀'
assert text[1:2] == "😀"
assert bytes.size == 6ul
A Klyn String is an immutable object storing UTF-16 text with an explicit length. Its size, indexing,
iteration, and slicing operate on Unicode scalar values; indexing therefore returns a
Char. The code point U+0000 is ordinary text and may appear anywhere
in a string without truncation. Use Byte[] for binary payloads,
native buffers, and file or network data that has not yet been decoded as text.
sizeof(String) measures the fixed object layout, not its text length:
on 64-bit platforms it is 16 bytes, comprising the type descriptor pointer and
the immutable UTF-16 payload reference. The separately allocated payload holds
its lengths and two bytes per code unit; no four-byte reserve per character is
imposed. sizeof(text) uses the same fixed layout.
text = m"A😀B"
characters = text.codePoints
assert characters.size == 3
assert characters[1] == '😀'
# Mutation requires MutableString and a scalar Char.
text[0] = 'B'
assert text == "B😀B"
# codeUnits exposes a snapshot of the 16-bit UTF-16 storage.
units = text.codeUnits
high as CodeUnit = units[1]
low as CodeUnit = units[2]
assert units.size == 4
assert high == `\uD83D`
assert low == `\uDE00`
codePoints returns a new Char[] snapshot and
codeUnits returns a new CodeUnit[] UTF-16 snapshot. The scalar count is
available through size; the UTF-16 length is the snapshot's size.
Keep that snapshot in a local variable when several code units are needed because repeatedly
evaluating either array property would allocate a new array each time.
Character formatting is textual by default. Width and alignment therefore count displayed scalar text rather than formatting the numeric code point.
assert f"[{'é':<3}]" == "[é ]"
assert f"[{`é`:<3}]" == "[é ]"
Validate the external encoding before creating a String. Keep arbitrary C
buffers and protocol frames as Byte[] until decoded. APIs requiring a
zero-terminated C string, such as operating-system paths, reject embedded
U+0000 rather than silently truncating it.
Generic Types in Declarations
import klyn.collections
names as ArrayList<String> = ["Ada", "Grace"]
config as Map<String, Int> = {"port": 8080}
values as IList<Int> = i:[1, 2, 3]
linked as List<Int> = l:[1, 2, 3]
pair as Tuple<String, Int> = ("age", 42)
Generic arguments are part of the declared type. Literals often infer them automatically, but
explicit generic annotations are usually clearer in public APIs. In Klyn,
IList<T> is the read-only indexed homogeneous collection contract,
List<T> is the mutable indexed contract, Map<K,V> is the
non-instantiable associative contract, and Tuple<...Ts> denotes a fixed positional
tuple. A map literal supplies a concrete implementation assignable to that contract.
Inspecting a Type Quickly
value = [10, 20, 30]
metadata = type(value)
print(metadata.fullName)
The full reflection API is documented later, but type(...) is already useful when
you want to verify what the compiler or runtime considers the real type of a value.
type(value) == Int checks the exact runtime type, whereas
value is Int tests membership in that type, including inheritance and
implemented interfaces. Neither test performs numeric conversions. An
Object variable keeps its declared static type even when it contains an
Int. A null reference is not an instance of any type.
value as Object = 42
assert type(value) == Int
assert value is Int
assert value is Object
assert value is not Double
assert 42 is not Double # Assignment conversion does not change type identity.
Common Mistakes
- Expecting inference to allow later assignments of unrelated types.
- Writing redundant
static constinstead of a class-levelconst. - Assuming that
constrecursively freezes a reference type. - Confusing a constant binding with an expression that the compiler can evaluate at compile time.
- Using
readonlyfor a class-owned field instead ofconst. - Relying on implicit numeric narrowing instead of converting explicitly.
- Testing NaN with equality instead of using its unordered comparison semantics.
- Assuming mathematically equal values of different primitive types are the same boxed map key.
- Leaving externally visible properties untyped because “the compiler can infer it anyway”.