EK9 Built in Types

EK9 has a set of built-in types and a set of collection types. Both were designed in from the start and form the core of the language.

Further reading on the blog: why strong typing matters, and the case for more sophisticated types in a programming language.

The Types

These are the basic building blocks. The predefined classes for specific purposes, more like a standard library, are in collection types, standard types and network types.

String and Bits are also a kind of collection: a String is an ordered list of Characters and Bits an ordered list of Booleans.

Every type here is an object, not a primitive, and is passed by reference, not by value. A variable of any type holds an object that is either set or un set: un set means the object exists but the value it holds is meaningless. The operators and basics pages cover this, and this page uses the terms of declarations.

EK9 has no static methods. Functions that return an object in a particular state, and methods that return a value without changing their object, do that job; the design note on small type APIs explains why.

Important: No 'null' Literal

EK9 has no null literal; writing null is error E01073. Every value is an object, and the question is whether it holds a meaningful value:

  • Unset - the object exists but has no meaningful value (for example Integer(), or the default handed back when a key is not in a dictionary)
  • Set - the object exists with a valid, usable value

Absent - no memory allocated to hold a value at all - is what other languages call null. EK9 is designed so that a program can never observe it: you test for set with the ? operator or a guard, never for null.

//Creating an unset value (NOT null)
name <- String()      //Unset String
require not name?     //_isSet() returns false

//Checking if set before use
chosen <- getName()
if chosen?
  process(chosen)     //Safe - guaranteed set

//Guard for safe access
if validName <- getName()
  process(validName)  //Safe - guaranteed set

The aim is to eliminate NullPointerException entirely: the compiler checks that every variable, property and return value holds an object before it is read. Making that a proven guarantee (closing the known gaps in those checks) is work in progress; Excluded Features gives the evidence behind the approach.

Any

Any is the implicit supertype of every EK9 construct: classes, records, components, traits, functions, enumerations, constrained types and generic instantiations.

Unlike Java's Object

In Java, Object is a class and everything extends it as a class. EK9's Any preserves the 'genus' (kind) of whatever it holds:

That makes Any a little "strange" next to other languages: it holds any construct and keeps what that construct is.

Limited Operations

Any has one operator, ? (is set), and no other methods, operators or call semantics. To work with the value you need its real type.

Type Detection with Dispatchers

EK9 has no casting and no instanceof. When you need the actual type of an Any, use a dispatcher method, which routes to the overload for the runtime type. See Advanced Class Methods.

Use Cases for Any

//A heterogeneous list - declare List of Any (an undeclared mix sharing only Any is E50030)
mixedList as List of Any: [ 1, "hello", 2024-01-15, CardSuit.Hearts ]
//To process these, use a dispatcher - see Advanced Class Methods

Boolean

A Boolean is one of:

Most languages have only true and false.

//Declare a Boolean - using type inference but with no value yet known.
reviewAccess <- Boolean()
//Declare a Boolean - with type inference with a known initial value
allowModification <- false
//Declare a Boolean - but with no space allocated to hold the value.
provideAccess as Boolean?
//Declare a Boolean - but with no value yet known.
denyAccess as Boolean: Boolean()
//Declare a Boolean - without type inference
allowViewing as Boolean := true

Boolean is the simplest type. Its operators are in the operators section. Use type inference wherever you can.

Character

A Character is one of:

For a word or a sentence, use a String.

//Some examples of using Character, some with type inference
c1 <- Character()
c2 <- 's'
c3 as Character := 'é'

//Comparisons
lessThan <- c1 < c2
lessOrEqual <- c1 <= c2
greaterThan <- c1 > c2
greaterOrEqual <- c1 >= c2
equal <- c1 == c2
notEqual <- c1 != c2
alsoNotEqual <- c1 <> c2

//Other operators
length1 <- length c1 //results in not set
length2 <- length c2 //results in 1
length3 <- length c3 //results in 1

length4 <- c3.length() //same result (1) - but written in object form
hashCode1 <- #? c3 //results in 233 (hashcode method)
hashCode2 <- c3.#?() //same result (233) - but written in object form

//Promotion to String
str1 as String := #^c2 //results String of "s"
str2 <- #^c2 //also results String of "s" - uses type inference
str3 <- c2.#^() //also results String of "s" - uses type inference and object form

//Standard methods
upper <- c2.upperCase() //results in 'S'
lower <- c2.upperCase().lowerCase() //results in 's'

String

A String is one of:

A String is an ordered sequence of characters. It can be changed and can grow, and it has no termination character. It has the comparison and functional operators. Note the distinction between not set and "", the empty String. It has:

And these methods:

ek9 -h String lists the full API.

String Literals

A String literal takes two forms: a fixed String and an interpolated String.

//Declare a fixed String literal
language <- "EK9"

//Declare a variable to be used in an interpolated String
age <- 30
//Use the variable in an interpolated String
message <- `Your age is ${age}`

An interpolated String starts and ends with a back tick '`'. Inside it, '${' starts a variable, function, method or expression for the compiler to evaluate, the same syntax as JavaScript. The result must be a String, something that can be coerced to a String, or a type with a '$' String operator.

Escapes in String

In a normal String such as "A Test", escape the " character: "A \"Test\"". The back tick '`' and '$' need no escape.

A normal String is never interpolated, so it cannot hold '${': "${count} items" would print those characters rather than the count, and the compiler rejects it (E07901). Double quotes do interpolate in Kotlin, Groovy, Dart, PHP and the shells, which is why it is easy to write by habit; before the error existed it compiled silently and showed up only as wrong output. The error message spells out both rewrites:

In an interpolated String the escaping is the other way round: `Your \`salary\` is \$${salary}`. Every '$' that does not open an interpolation is written \$, including a regular expression replacement that names a group, `\${day}.\${month}`.

A handful of Strings can sit in the source. For very large text, or many text elements in different spoken languages, use the text construct: it keeps plain and interpolated Strings in their own construct, away from the code.

Interpolation Example

See text properties for defining a large number of String text items.

#!ek9
defines module site.builtInTypes.interpolation

  defines record
    Person
      firstName String: String()
      lastName String: String()

      Person()
        ->
          firstName String
          lastName String
        require firstName? and lastName?

        this.firstName :=: firstName
        this.lastName :=: lastName

      operator $ as pure
        <- rtn String: `${firstName} ${lastName}`

      default operator ?

  defines function
    getAuthor()
      <- rtn Person: Person("Steve", "Limb")

  defines program

    ShowStringInterpolation()
      stdout <- Stdout()

      normal <- "With normal double quote \" text"
      stdout.println(`[${normal}]`)

      var <- 90.9
      interpolated <- `A tab\t ${var} a " but a back tick \` the dollar have to be escaped \$\nOK?`
      stdout.println(`[${interpolated}]`)

      me <- Person("Steve", "Limb")
      stdout.println(`Author: ${me} or via function: ${getAuthor()}, direct access "${me.firstName}"`)

It prints:

[With normal double quote " text]
[A tab	 90.9 a " but a back tick ` the dollar have to be escaped $
OK?]
Author: Steve Limb or via function: Steve Limb, direct access "Steve"

Why EK9 does it this way. The two String literals are deliberately different, " and `, so you choose one or the other. A variable in an interpolated String is always written ${...}, never a bare $. Because the two forms are distinct the compiler can hold the line between them: ${ inside a " String is an error (E07901), never quietly treated as text.

Accessing Characters in a String

To take a Character or a range of Characters from a String, use a pipeline:

#!ek9
defines module site.builtInTypes.string

  defines function

    isO() as pure
      -> c as Character
      <- o as Integer: c == 'o' or c == 'O' <- 1 else 0

  defines program

    ShowStringType()
      stdout <- Stdout()

      i1 <- "The Quick Brown Fox"
      i2 <- "Jumps Over The Lazy Dog"
      space <- " "
      i3 <- String()
      stdout.println(`i3 is set? ${i3?}`)

      hashCode <- #? i1
      stdout.println(`Hashcode is ${hashCode}`)

      sentence1 <- `${i1} ${i2}`
      stdout.println(`sentence1 is "${sentence1}"`)

      l1 <- length sentence1
      l2 <- sentence1.length() //the same value, but via the method
      stdout.println(`Length is ${l1} or ${l2}`)

      oCount <- sentence1.count('o') //only the lower case 'o'
      eitherCaseOCount <- cat sentence1 | map with isO | collect as Integer //'O' and 'o'
      stdout.println(`'o' appears ${oCount} times, 'o' or 'O' ${eitherCaseOCount} times`)

      //Alternative way of joining values

      parts <- [i1, space, i2]
      sentence2 <- cat parts | collect as String

      //OR
      sentence3 <- cat [i1, space, i2] | collect as String
      stdout.println(`sentence2 and sentence3 are the same? ${sentence2 == sentence3}`)

      //Mechanism to extract parts of a String
      jumpingBrownFox <- cat sentence2 | skip 10 | head 15 | collect as String
      stdout.println(`jumpingBrownFox is "${jumpingBrownFox}"`)

      //Note that there is no issue with extending beyond the length of the sentence
      dog <- cat sentence2 | skip 40 | head 15 | collect as String
      stdout.println(`dog is "${dog}"`)

      //Even this is 'safe' and the result is just not set
      nonSuch <- cat sentence2 | skip 50 | head 5 | collect as String
      stdout.println(`nonSuch is set? ${nonSuch?}`)

It prints:

i3 is set? false
Hashcode is -732416445
sentence1 is "The Quick Brown Fox Jumps Over The Lazy Dog"
Length is 43 or 43
'o' appears 3 times, 'o' or 'O' 4 times
sentence2 and sentence3 are the same? true
jumpingBrownFox is "Brown Fox Jumps"
dog is "Dog"
nonSuch is set? false

The example shows:

There is no index operator for the Characters of a String. Selection is by pipeline, or by position with subString(start, end) and indexOf, and none of them can run past the end: in the example, nonSuch is un set, and a subString out of range is un set too.

Most object-oriented languages keep adding methods to their types for jobs like this. EK9 puts the very frequent ones on the type (upper and lower case) and gives you the tools to write the rest as your own functions; see the design note on small type APIs.

Integer

An Integer is one of:

The comparison, mathematical , modification and ternary operators are covered on their own pages; here are a few examples. There is no plus or minus infinity (or NaN) in EK9: an infinity is not known, and that is un set.

//Some examples of using Integer
i1 <- -9223372036854775808
hashCode <- #? i1 //results in -2147483648
asFloat <- #^ i1 //promotion to Float results in -9.223372036854776E18

//Division operations with zero
nonResult <- zero / ninety //result is 0 (zero)
nonResult := ninety / zero //result is un set i.e not known
nonResult := zero / alsoZero //result is un set

There are no unsigned, short or long variants. Integer is the 64-bit whole number; for whole numbers too large for it use BigInteger, and for a raw unsigned octet use Byte.

Pipeline with Integer result

Accumulating amounts is common, and many languages have a special syntax for it. EK9 has no list comprehensions; it has the more general streams/pipelines.

The example below makes a list of Integers from 1 up to and including 11 in steps of two, then totals them. A for loop could do it; here it is a pipeline. The key part is collect as Integer, and you can collect into your own types too.

theValues as List of Integer := List() //create a list to capture values in

//Simple pipeline processing
intSum <- for i in 1 ... 11 by 2 | tee in theValues | collect as Integer

//theValues has the following contents [1, 3, 5, 7, 9, 11]
//intSum has the value 36

The pipeline approach gives reuse, and collection of objects as they are processed, in one standard syntax. The same pipeline works with Durations (covered later) instead of Integers: processing, accumulation, mapping and teeing are the same whatever the type. The syntax is admittedly not as terse as a list comprehension.

Float

A Float is one of:

As with Integer, the comparison, mathematical , modification and ternary operators are covered on their own pages.

//Some examples of using Float
i1 <- -4.9E-324
hashCode <- #? i1 //results in -2147483647

//Division operations with zero
nonResult <- zero / ninety //result is 0.0 (zero)
nonResult := ninety / zero //result is un set i.e not known
nonResult := zero / alsoZero //result is un set

Float has most of Integer's operators, except promotion '#^', and its precision varies with the magnitude. It works in pipelines too.

It also has trigonometric and transcendental methods, in radians, from the platform maths library: sin, cos, tan, asin, acos, atan, atan2, log (natural), log10 and exp, plus round(places). A result that is not a finite number (for example asin of a value outside -1..1) is un set. The constants pi and e are on Maths.

BigInteger

BigInteger is an arbitrary-precision integer. Integer is 64-bit and becomes un set on overflow; a BigInteger grows as large as needed, so '+', '-', '*' and '^' (power) never overflow. Use it for big factorials, unbounded Fibonacci or large combinatorics.

It has the same comparison and mathematical operators as Integer, plus sqrt and abs. mod and rem return an Integer (as EK9 requires); a result that will not fit in 64 bits is un set rather than silently losing data. There is deliberately no promotion ('#^'): a BigInteger is wider than a Float, so an implicit conversion would lose precision.

#!ek9
defines module site.builtInTypes.bigInteger

  defines program

    BigIntegerExample()
      stdout <- Stdout()

      //A value far beyond the range of a 64-bit Integer
      big <- BigInteger("123456789012345678901234567890")

      //Multiplication never overflows
      squared <- big * big
      stdout.println("Squared is " + $squared)

BigDecimal

BigDecimal is an arbitrary-precision decimal, the exact counterpart of Float. Float is 64-bit binary and cannot represent values such as 0.1 exactly; a BigDecimal can, so 0.1 + 0.2 really is 0.3. Use it for exact or high-precision calculation (for money, use Money).

#!ek9
defines module site.builtInTypes.bigDecimal

  defines program

    BigDecimalExample()
      stdout <- Stdout()

      total <- BigDecimal("0.1") + BigDecimal("0.2")
      stdout.println("0.1 + 0.2 = " + $total)

Maths

Maths is a small stateless type holding the mathematical constants as Float values, pi() and e(). Create one with Maths() and read them; the trigonometric and transcendental functions are on Float.

#!ek9
defines module site.builtInTypes.maths

  defines program

    MathsExample()
      stdout <- Stdout()

      maths <- Maths()

      //sin of pi/2 is 1.0
      halfPi <- maths.pi() / 2.0
      stdout.println("sin(pi/2) = " + $halfPi.sin())

Bits

Bits is one of:

The bitwise operators are in the operators section. '+' and '+=' join Bits together, as does '|' in a stream/pipeline.

//Bits values
a <- 0b010011
b <- 0b101010
c <- 0b010011

//Equality Operators
equalAB <- a == b //result would be false
notEqualAB <- a <> b //result would be true
lessAB <- a < b //result would be true
lessOrEqualAB <- a <= b //result would be true
greaterAB <- a > b //result would be false
greaterOrEqualAB <- a >= b //result would be false
equalAC <- a == c //result would be true
notEqualAC <- a != c //result would be false
alsoNotEqualAC <- a <> c //result would be false (alternate syntax)

//Joining/Adding Bits
set6 <- 0b010011
set7 <- 0b101010

set6set7 <- set6 + set7 //result would be 010011101010
set6true <- set6 + true //result would be 0100111
set6false <- set6 + false //result would be 0100110
set6false += true //set6false would now be 01001101

//Bitwise operations
ored <- set6 or set7 //result would be 0b111011
xored <- set6 xor set7 //result would be 0b111001
anded <- set6 and set7 //result would be 0b000010
notted <- ~set6 //result would be 0b101100
alsonotted <- not set6 //result would be 0b101100

As the addition shows, Bits are not numbers.

To take a range of bits, use a stream/pipeline:

//Extracting ranges of Bits
//assumes function booleanToBits has been defined
partial <- cat set6false | skip 3 | map with booleanToBits | collect as Bits
//partial has the value of 0b10010

Bits read right to left: the least significant bit is on the right and the most significant on the left, so streaming the bits (as Boolean values) starts with the least significant. In the example the "101" is skipped, leaving "01001". The bits arrive least significant first and each one collected is added on the right (as + adds a bit), so the result reads 0b10010, the remaining bits in reverse. Use head and tail to narrow the selection further.

The same functional approach selects from any sequence, so it is repeatable and consistent rather than a set of special methods per class. The design note on small type APIs explains the thinking.

Byte

Byte is an unsigned 8-bit value, 0 to 255, the scalar of the byte family. It gives raw binary data an element type with real EK9 semantics (value semantics, immutability and tri-state) rather than a naked small integer.

#!ek9
defines module site.builtInTypes.byte

  defines program

    ByteExample()
      stdout <- Stdout()

      //Hex text, an Integer, or a copy
      fromHex <- Byte("0xFF")
      fromInteger <- Byte(255)

      //$ always emits two upper-case hex characters, so the round trip holds
      stdout.println($fromHex)                  //FF
      stdout.println($Byte(15))                 //0F

      //Out of range is un set, never wrapped
      stdout.println(`Byte(256) is set: ${Byte(256)?}`)

      //Bitwise, and shifts that stay inside eight bits
      masked <- Byte("AA") and Byte("0F")       //0A
      stdout.println($masked)
      stdout.println($(Byte("0F") << 4))        //F0
      stdout.println($(Byte("0F") << 8))        //00 - shifted out, not widened

      //Arithmetic is deliberate - and widens rather than wrapping
      stdout.println(`1 + 0xFF = ${1 + #^fromInteger}`)   //256

      //Conversions
      stdout.println($fromInteger.toInteger())  //255
      stdout.println($Byte("AA").toBits())      //10101010

Bytes

Bytes is an immutable sequence of unsigned 8-bit values, the public face of binary data. It follows the sequence semantics of Bits, not a number's: '+' concatenates, '|' accumulates, and there is no arithmetic.

#!ek9
defines module site.builtInTypes.bytes

  defines program

    BytesExample()
      stdout <- Stdout()

      //Hex in, hex out - the round trip always holds
      payload <- Bytes("DEADBEEF")
      stdout.println(`${payload} has ${length payload} bytes`)
      stdout.println(payload.toBase64())

      //Buffers via the 'with' factory convention
      mask <- Bytes().withLength(4, Byte("FF"))

      //xor is its own inverse - the workhorse of low level code
      masked <- payload xor mask
      stdout.println(`masked ${masked}, recovered ${(masked xor mask) == payload}`)

      //Unequal lengths are un set, never zero extended
      stdout.println(`short key set: ${(payload xor Bytes("FF"))?}`)

      //'+' concatenates and slices are values, not views
      stdout.println($(Bytes("DEAD") + Bytes("BEEF")))
      stdout.println($payload.first(2))

      //Shifts cross byte boundaries and keep the length
      stdout.println($(payload << 4))

      //Grow to make room, then shift the content into it
      stdout.println($(Bytes("DEAD").growLeft(2) << 16))

BytesReader

BytesReader is a forward-only cursor over a Bytes, for parsing binary protocols and formats. Get one with aBytes.reader() or BytesReader(aBytes). A reader constructed with no source is unset and has nothing to read.

Why EK9 does it this way. A silent endianness default is a known bug category: it is right on the machine it was written on and wrong in production. EK9 removes bug categories by making them unrepresentable, so the defaulted spelling does not exist.

#!ek9
defines module site.builtInTypes.bytesReader

  defines program

    BytesReaderExample()
      stdout <- Stdout()

      //A TLS-record shape: type, version, body length, then a length-prefixed body
      record <- Bytes("1603030008034142430358595a")
      cursor <- record.reader()

      stdout.println(`type ${cursor.readByte()}, version ${cursor.readUInt16BE()}`)
      bodyLength <- cursor.readUInt16BE()

      //A bounded view - it cannot read past its own boundary
      body <- cursor.slice(bodyLength)
      fieldLength <- body.readByte().toInteger()
      stdout.println(`first field: ${body.readBytes(fieldLength)}`)

      //A short read is un set and does NOT move the cursor
      short <- Bytes("010203").reader()
      stdout.println(`wide read set: ${short.readUInt32BE()?}`)
      stdout.println(`still at ${short.position()}, smaller read gives ${short.readUInt16BE()}`)

BytesBuilder

BytesBuilder is the mutable working buffer, the write side of the family and the counterpart of BytesReader. It is the only type here that allows an indexed byte write, and it exists so that capability does not leak into Bytes or general application code.

🔑 Speed is not why it exists. Bytes is a value, so every + and | copies the whole accumulator and building byte by byte is quadratic; but that is correct, only slow, and fine at protocol sizes. Three capabilities cannot be expressed on Bytes at any speed, and they are the reason:

  1. Endian-aware writes. appendUInt16BE/LE, appendUInt32BE/LE, appendUInt64BE/LE and the signed appendInt16/32/64BE/LE: twelve writes matching BytesReader's twelve reads one for one. Without them there is no endian-aware write anywhere in the language, so callers would hand-assemble bytes and bring back the endianness bug the reader removed. As on the read side, there is no defaulted appendUInt32().
  2. In-place mutation: setByte(index, aByte), xorInto(offset, source) and copyInto(offset, source). A value type cannot offer these, because every operation on Bytes yields a new array. xorInto is one primitive with three uses: AES AddRoundKey, the Argon2 block XOR and the ChaCha20 keystream. All three write within the current length and never extend; use append to grow.
  3. wipe(). A Bytes cannot be securely erased, because + and | have already scattered copies of it. A builder is one buffer that can be zeroed, which key material and password buffers need. It is best effort: the JVM may have moved the buffer during garbage collection and the OS may have paged it out.
#!ek9
defines module site.builtInTypes.bytesBuilder

  defines constant

    tlsVersionTls12 <- 0x0303
    tlsBodyLength <- 8

  defines program

    BytesBuilderExample()
      stdout <- Stdout()

      //Building the TLS-record shape that the BytesReader example parses. The endianness is in
      //the method name, so it is visible on the line doing the writing.
      record <- BytesBuilder()
      record.append(Byte("16"))
      record.appendUInt16BE(tlsVersionTls12)
      record.appendUInt16BE(tlsBodyLength)
      record.append(Bytes("034142430358595a"))
      stdout.println(`framed: ${record.toBytes()}`)

      //Big and little endian genuinely differ - there is no defaulted spelling
      bigEndian <- BytesBuilder()
      bigEndian.appendUInt32BE(0x01020304)
      littleEndian <- BytesBuilder()
      littleEndian.appendUInt32LE(0x01020304)
      stdout.println(`big ${bigEndian.toBytes()}, little ${littleEndian.toBytes()}`)

      //A number too wide for its field is rejected, never truncated into a quiet lie
      tooWide <- BytesBuilder()
      tooWide.appendUInt16BE(0x10000)
      stdout.println(`too wide accepted: ${tooWide?}`)

      //xorInto is AddRoundKey, the Argon2 block XOR and the ChaCha20 keystream application,
      //and it is its own inverse
      block <- BytesBuilder(Bytes("ffffffff"))
      block.xorInto(0, Bytes("0f0f0f0f"))
      stdout.println(`after xor: ${block.toBytes()}`)

      //wipe zeroes the backing store and leaves the builder usable - something an immutable
      //Bytes fundamentally cannot offer
      secret <- BytesBuilder(Bytes("cafebabedeadbeef"))
      secret.wipe()
      stdout.println(`wiped to length ${length secret}, still usable ${secret?}`)

Digest

Digest produces unkeyed cryptographic hashes. It answers one question: has this content changed? That is all this capability has ever been, and Digest is its honest name. A digest proves integrity and nothing else: anyone can compute one, so anyone who can replace your content can replace the digest beside it too.

🔑 To prove WHO produced something, this is the wrong type: use HMAC. A hash over an attacker's content is exactly as valid as a hash over yours, and that mistake is what splitting the two types prevents.

HMAC

HMAC is a keyed message authentication code. It proves integrity AND authenticity, because producing one needs the key: SHA256(key, message), where both are Bytes and so is the result.

Why EK9 does it this way. The key is not optional, and that is the point. Until the Bytes family existed there was no honest way to express a keyed MAC, and this type carried the unkeyed hashes instead, so code reaching for HMAC got a bare digest and the authenticity it assumed was imaginary. Those hashes now live on Digest, and this type means what its name says.

The example uses RFC 4231 test case 2, a published vector: it agrees with every conforming implementation or it is wrong. A value captured from our own output would pass equally well over a subtly broken MAC.

#!ek9
defines module site.builtInTypes.digestHmac

  defines constant

    sha256HexLength <- 64

  defines program

    DigestAndHmacExample()
      stdout <- Stdout()

      //Digest is UNKEYED - anyone can compute it, so it proves only integrity
      fingerprint <- Digest().SHA256("the quick brown fox")
      stdout.println(`fingerprint length ${length fingerprint}, expected ${sha256HexLength}`)

      //The Bytes overload hashes OCTETS and answers in octets - no encoding assumption
      //anywhere, so it works on a binary payload as readily as on text
      payload <- Bytes().withUtf8("the quick brown fox")
      asOctets <- Digest().SHA256(payload)
      stdout.println(`same value both ways: ${asOctets.toText("hex") == fingerprint}`)

      //HMAC is KEYED - producing one requires the secret, so it proves WHO
      key <- Bytes().withUtf8("Jefe")
      message <- Bytes().withUtf8("what do ya want for nothing?")
      mac <- HMAC().SHA256(key, message)
      stdout.println(`mac: ${mac}`)

      //🔑 Change the key and the MAC changes completely. That is the whole difference:
      //a Digest anyone can forge over new content, an HMAC they cannot without the key.
      forged <- HMAC().SHA256(Bytes().withUtf8("wrong-key"), message)
      stdout.println(`a wrong key cannot reproduce it: ${mac <> forged}`)

      //⚠️ VERIFY WITH constantTimeEquals, never with ==. Both answer the same question, but
      //== is free to stop at the first differing byte, and how long it took then tells an
      //attacker how many leading bytes they guessed right - a tag forged one byte at a time.
      stdout.println(`verified: ${mac.constantTimeEquals(HMAC().SHA256(key, message))}`)
      stdout.println(`forgery rejected: ${mac.constantTimeEquals(forged)}`)

      //Unset in, unset out - never a MAC computed over a substitute value
      stdout.println(`unset key gives unset mac: ${HMAC().SHA256(Bytes(), message)?}`)

It prints:

fingerprint length 64, expected 64
same value both ways: true
mac: 5BDCC146BF60754E6A042426089575C75A003F089D2739839DEC58B964EC3843
a wrong key cannot reproduce it: true
verified: true
forgery rejected: false
unset key gives unset mac: false

SecureRandom

SecureRandom produces cryptographically secure random octets, for nonces, salts, keys and tokens. It has one operation, nextBytes(count), which returns count octets as Bytes. A token is those octets as text: nextBytes(32).toBase64() or .toText("hex"). There is deliberately no next() returning an Integer: reducing random numbers into a range by hand is how modulo bias gets into tokens.

Tri-state: an unset or negative count gives unset Bytes, never a substitute length. A count of zero gives empty Bytes, which are set: asking for nothing and getting nothing is a well-defined answer.

Why EK9 does it this way. 🔑 SecureRandom is deliberately not a Random. SystemRandom and SeededRandom implement the Random trait; SecureRandom does not. If it did, anything accepting a Random would accept a SeededRandom too, and a seeded generator gives the same sequence every run, so a nonce or salt made from it is reproducible by anyone who knows the seed. Standing outside the trait, a function that needs unpredictable octets declares a SecureRandom parameter, and passing it a SeededRandom is a compile error rather than a code-review catch. For the same reason there is no seeded constructor.

#!ek9
defines module site.builtInTypes.secureRandom

  defines function

    //Declaring SecureRandom is the safety property - a SeededRandom will not compile here
    newSalt() as pure
      ->
        generator as SecureRandom
      <-
        rtn as Bytes: generator.nextBytes(16)

  defines program

    SecureRandomExample()
      stdout <- Stdout()
      generator <- SecureRandom()

      //Exactly the number of octets asked for
      salt <- newSalt(generator)
      stdout.println(`salt length: ${length salt}`)

      //A token is octets rendered as text - there is no next() Integer on purpose
      token <- generator.nextBytes(32).toBase64()
      stdout.println(`token length: ${length token}`)

      //Two generators never agree - there is no seed for them to share
      stdout.println(`generators differ: ${SecureRandom().nextBytes(32) <> SecureRandom().nextBytes(32)}`)

      //Zero is EMPTY and SET; an unset or negative count is UNSET
      stdout.println(`empty is set: ${generator.nextBytes(0)?}`)
      stdout.println(`negative count: ${generator.nextBytes(-1)?}`)

It prints:

salt length: 16
token length: 44
generators differ: true
empty is set: true
negative count: false

Time

A Time is one of:

Time is a time of day, HH:MM or HH:MM:SS, with no date. A time of day on a particular date in a particular time zone is a DateTime.

The comparison and ternary operators are covered on their own pages. Time works with Duration:

The example also shows the require keyword, for pre-conditions and post-conditions.

#!ek9
defines module site.builtInTypes.time

  defines program

    ShowTimeType()

      //These declarations and operations should be obvious by now if you've read the other sections.

      t1 <- 12:00
      t2 <- 12:00:01
      t3 <- 12:00:01
      t4 <- Time()
      t5 <- Time(12, 00)
      t6 <- Time(12, 00, 01)

      require t1 <> t2
      require t2 == t3

      require t1 < t2
      require t1 <= t2
      require t2 <= t3

      require t2 > t1
      require t2 >= t1
      require t3 >= t2

      //Check not set
      require ~t4?

      require t1 == t5
      require t2 == t6

      //Just create a Time object to get the current time
      t4a <- SystemClock().time() //t4a will be set to current time
      t4b <- t4.startOfDay() //t4b will be set to 00:00:00
      t4c <- t4.endOfDay() //t4c will be set to 23:59:59
      require t4a?
      Stdout().println(`Start of day ${t4b}, end of day ${t4c}`)

      //Now actually alter t4 itself
      t4.set(SystemClock().time()) //t4 will now be set to the current time
      require t4?
      t4.setStartOfDay() //Will be set to 00:00:00
      require t4?
      t4.setEndOfDay() //Will be set to 23:59:59
      require t4?

      //Durations
      d1 <- PT1H2M3S //one hour, two minutes and three seconds
      d2 <- P3DT2H59M18S //three days, two hours, fifty nine minutes and eighteen seconds

      //Note how only the hours minutes and seconds of 'd2' is used.

      //Addition operations
      t4 := t5 + d1
      require t4 == 13:02:03
      t4 += d2
      require t4 == 16:01:21

      //Subtraction operations
      t4 := t5 - d1
      require t4 == 10:57:57
      t4 -= d2
      require t4 == 07:58:39

      //Get the Duration between two times
      d3 <- 12:04:09 - 06:15:12
      require d3 == PT5H48M57S

      //Note the different views of a Duration
      d4 <- 06:15:12 - 12:04:09
      require d4 == PT-6H11M3S
      require d4 == PT-5H-48M-57S

      //Example of building a time from a number of durations
      durations as List of Duration := [d1, d2]

      t7 <- cat durations | collect as Time
      require t7 == 04:01:21

      //t7 will default to start of the day plus each of the durations piped in
      //Note that time will just roll over just after 23:59:59

      //It is also possible to get the hour, minute and second from a Time
      second <- t7.second()
      minute <- t7.minute()
      hour <- t7.hour()

      require 4 == hour
      require 1 == minute
      require 21 == second

Duration

A Duration is one of:

With Time only hours, minutes and seconds matter, but a Duration can also hold years, months and days, which matters for Date and DateTime.

The format is ISO 8601 Durations, P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S, without fractional values (see Millisecond for fractions of a second). 'P' marks a period, and each '[n]' is followed by its field: "P3Y6M4DT12H30M5S" is three years, six months, four days, twelve hours, thirty minutes and five seconds. 'W' gives weeks.

To a person, anything under a second is a different category of time, so Duration has no milliseconds. Milliseconds can still add up to something significant, so Millisecond and Duration are separate types designed to work together.

//Some examples of using Duration
d1 <- PT1H2M3S //results in 1 hour, 2 minutes and 3 seconds
d2 <- P3DT2H59M18S //results in 3 days, 2 hours, 59 minutes and 18 seconds
d3 <- P2Y6W3DT8H5M8S //results in 2 years, 45 days, 8 hours, 5 minutes and 8 seconds
//Note that 6 weeks was converted to 42 days and then the additional 3 days are added to that
d4 <- P2Y2M1W3DT8H5M8S //results in 2 years, 2 months, 10 days, 8 hours, 5 minutes and 8 seconds
d5 <- P2W //results in 14 days

//Mathematics with durations
d6 <- PT1H2M1S
twice <- d6 * 2 //results in PT2H4M2S i.e. twice the duration in d6
scaled <- d6 * 8.25 //results in PT8H31M38S
divided <- scaled / 3 //results in PT2H50M32S
extended <- d6 + PT20M //results in PT1H22M1S
reduced <- d6 - PT2M6S //results in PT59M55S
reduced += PT2H5S //results in PT3H

A Duration takes ordinary arithmetic, but only where it makes sense: you can add and subtract Durations, and multiply or divide one by an Integer or Float, but not add an Integer, Float or String to it. Operators overloaded with suitable types like these, on the built-in types and on your own records and classes, are what keep EK9 readable.

Millisecond

A Millisecond is one of:

The literal is digits then 'ms': 250ms is two hundred and fifty milliseconds. The compiler checks millisecond literals. Any integer is valid, positive or negative, so 6000ms is 6 seconds.

Date

A Date is one of:

The literal is YYYY-MM-DD: 2020-11-18 is the eighteenth of November 2020. The compiler checks date literals, so '2020-11-31' does not compile (E07900).

A Date has no time zone, so it suits a date of birth, for example. To be exact about an instant, use a DateTime.

Its operations match Time's: comparison, ternary, and arithmetic with a Duration ('+', '-', '+=' and '-=' take a Duration; one Date minus another gives a Duration; adding, multiplying or dividing by a Date is not offered).

#!ek9
defines module site.builtInTypes.date

  defines program

    ShowDateType()

      //3rd of October 2020
      date1 <- 2020-10-03
      date2 <- 2020-10-04
      date3 <- 2020-10-04
      date4 <- Date()
      date5 <- Date(2020, 10, 03)
      date6 <- Date(2020, 10, 04)

      require date1 <> date2
      require date2 == date3

      require date1 < date2
      require date1 <= date2
      require date2 <= date3

      require date2 > date1
      require date2 >= date1
      require date3 >= date2

      require ~date4?

      require date1 == date5
      require date2 == date6

      date4a <- Date().today()
      require date4a?

      date4.setToday() //Now set to the current date

      //Durations
      d1 <- P1Y1M4D //one year, one month and four days
      d2 <- P4W1DT2H59M18S //twenty nine days, two hours, fifty nine minutes and eighteen seconds

      //Note how only the days part of 'd2' is used
      date4 := date5 + d1
      require date4 == 2021-11-07

      date4 += d2
      require date4 == 2021-12-06

      d3 <- date4 - date5
      require d3 == P1Y2M3D

      //Pipeline with durations and dates.
      durations as List of Duration := [d1, d2]

      //Because the epoch is 1970-01-01 accumulation starts from that date
      date7 <- cat durations | collect as Date
      require date7 == 1971-03-06

      //If you want to accumulate from a specific date you can do this instead
      date8 <- 0000-01-01
      cat durations >> date8
      require date8 == 0001-03-06

      dayOfMonth <- date8.day()
      monthOfYear <- date8.month()
      year <- date8.year()
      dayOfTheWeek <- date8.dayOfWeek()

      require dayOfMonth == 6
      require monthOfYear == 3
      require year == 1
      require dayOfTheWeek == 2

Take care when adding durations to points in time and subtracting points in time to get durations back; there are some strange effects:

To get a final date from a base date and several durations, apply each duration in turn to the base date rather than summing them first:

//Adding Durations to a Base Date
date <- 2019-06-01
//Definition of durations omitted
cat durations >> date
//The Easiest way to move a date on is by a number of durations

That way any number of positive, negative or even empty durations can be applied, and the date moves by the right amount from the base date it started at, including leap years and the varying days per month.

DateTime

DateTime is an aggregate of sorts: it holds an instant in time together with a time zone, so it relates a Date, a Time and a time zone in one type.

A DateTime is one of:

The literal is YYYY-MM-DDTHH:MM:SS+/-HH:MM: 2020-11-18T13:45:21-01:00 is the eighteenth of November 2020 at thirteen forty-five and twenty-one seconds, in a time zone one hour behind UTC. 'Z' (Zulu, GMT/UTC) is short for +/-00:00: 2020-11-18T14:45:21Z.

Time zones take a little more thought than Time and Date on their own. Is 2020-11-18T13:45:21-01:00 less than or greater than 2020-11-18T13:45:21Z? The "-01:00" may mislead you: it is greater. Adjust both to the same time zone (add the hour to the time of day) and it becomes obvious.

The operations match Date and Time: comparison, ternary, and calculation with a Duration.

The example uses time zones. Arithmetic over dates, times and time zones can be confusing; the thing to keep in mind is the same instant in time: if you are in London, what time (and date) is it right now in New York?

#!ek9
defines module site.builtInTypes.dateTime

  defines program

    ShowDateTimeType()

      //3rd of october 2020 @ 12:00 UTC
      dateTime1 <- 2020-10-03T12:00:00Z
      dateTime2 <- 2020-10-04T12:15:00-05:00
      dateTime3 <- 2020-10-04T12:15:00-05:00
      dateTime4 <- DateTime()
      dateTime5 <- DateTime(year: 2020, month: 10, dayOfMonth: 3, hour: 12)
      dateTime6 <- DateTime(year: 2020, month: 10, dayOfMonth: 4, hour: 12, minute: 15)

      require dateTime1 <> dateTime2
      require dateTime2 == dateTime3

      require dateTime1 < dateTime2
      require dateTime1 <= dateTime2
      require dateTime2 <= dateTime3

      require dateTime2 > dateTime1
      require dateTime2 >= dateTime1
      require dateTime3 >= dateTime2

      require ~dateTime4?

      require dateTime1 == dateTime5

      dateTime4a <- DateTime().today()
      require dateTime4a?

      dateTime4.setToday() //Now set to the current dateTime

      //Durations
      d1 <- P1Y1M4D //one year, one month and 4 days
      d2 <- P4W1DT2H59M18S //twenty nine days, two hours, fifty nine minutes and eighteen seconds

      dateTime4 := dateTime5 + d1
      require dateTime4 == 2021-11-07T12:00:00Z

      dateTime4 += d2
      require dateTime4 == 2021-12-06T14:59:18Z

      d3 <- dateTime4 - dateTime5
      require d3 == P1Y2M3DT2H59M18S

      d4 <- dateTime2 - dateTime3
      require d4 == PT0S

      //Think carefully about this (focus on the instant)
      //At ZULU/GMT/UTC what time will it be in the -05:00 time zone?
      //2020-10-04T12:15:00-05:00
      //2020-10-04T12:15:00Z
      d5 <- dateTime3 - dateTime6
      require d5 == PT5H

      d6 <- dateTime3.offSetFromUTC()
      require d6 == PT-5H

      //processing of durations
      durations as List of Duration := [d1, d2]
      dateTime7 <- cat durations | collect as DateTime
      //Because the epoch is 1970-01-01
      require dateTime7 == 1971-03-06T02:59:18Z

      //From a specific date time add on the durations in turn
      dateTime8 <- 0000-01-01T00:00:00Z
      cat durations >> dateTime8
      require dateTime8 == 0001-03-06T02:59:18Z

      dayOfMonth <- dateTime8.day()
      monthOfYear <- dateTime8.month()
      year <- dateTime8.year()
      dayOfTheWeek <- dateTime8.dayOfWeek()
      hourOfTheDay <- dateTime8.hour()
      minuteOfTheHour <- dateTime8.minute()
      secondOfTheMinute <- dateTime8.second()
      timeZone <- dateTime8.zone()
      offset <- dateTime8.offSetFromUTC()

      require dayOfMonth == 6
      require monthOfYear == 3
      require year == 1
      require dayOfTheWeek == 2
      require hourOfTheDay == 2
      require minuteOfTheHour == 59
      require secondOfTheMinute == 18
      require timeZone == "Z"
      require offset == PT0S

The example is long, but the ideas are those of Date and Time with Duration: ordinary mathematical operators with the same semantics whatever the type. That keeps down the number of method names, helper classes and third-party APIs you need.

Why EK9 does it this way. If you adopt EK9, go with the grain and flow of it. Don't fight it; if you find yourself doing so, a different language such as C or C# would suit you better. The design note on small type APIs sets out the reasoning.

Money

Money models a modern business concept, and many programming languages have no built-in type for it. EK9 is aimed at a wide range of uses, so it includes Money from the start. Money is harder to work with than a DateTime: the number of fractional parts (cents and so on) and the rounding vary from currency to currency.

A Money is one of:

The literal is D+(.D+)?#SSS: digits, an optional point and fractional part, then '#' and the three-letter ISO 4217 currency code.

The number of fractional places depends on the currency. The Chilean Unidad de Fomento has four (45.9999#CLF), the Iraqi Dinar three (45.000#IQD), GBP, USD and many others two, and the Guinea Franc none.

That literal format is for code; end users see Money through a Locale. How you present it is your choice: a currency may have two decimal places, but a fixed charge of £50 or $75 in your application may not need its pennies or cents shown.

Examples of Money:

#!ek9
defines module site.builtInTypes.money

  defines program

    ShowMoneyType()

      tenPounds <- 10#GBP
      require tenPounds == 10.00#GBP

      thirtyDollarsTwentyCents <- 30.2#USD
      require thirtyDollarsTwentyCents == 30.20#USD

      //Mathematical operators
      nintyNinePoundsFiftyOnePence <- tenPounds + 89.51#GBP
      require nintyNinePoundsFiftyOnePence == 99.51#GBP

      require tenPounds < nintyNinePoundsFiftyOnePence
      require nintyNinePoundsFiftyOnePence > tenPounds
      require tenPounds <> nintyNinePoundsFiftyOnePence
      require tenPounds <= 10.00#GBP
      require tenPounds >= 10.00#GBP

      //As they are different currencies - operation like this are meaningless
      //You will have to convert to the same currency to compare or combine them.
      require ~(tenPounds != thirtyDollarsTwentyCents)?
      require ~(tenPounds == thirtyDollarsTwentyCents)?
      //The above requirements are checking if the equality test is a valid test, not the result of the equality test.

      //rounding up for money values 49.755 is 49.76
      fourtyNinePoundsSeventySixPence <- nintyNinePoundsFiftyOnePence/2
      require fourtyNinePoundsSeventySixPence == 49.76#GBP

      minusFourHundredAndThirtyFivePoundsSixtyPence <- fourtyNinePoundsSeventySixPence * -8.754
      require minusFourHundredAndThirtyFivePoundsSixtyPence == -435.60#GBP

      nineHundredAndThirtyFivePoundsSixtyPence <- 500#GBP - minusFourHundredAndThirtyFivePoundsSixtyPence
      require nineHundredAndThirtyFivePoundsSixtyPence == 935.60#GBP

      variableAmount <- (nineHundredAndThirtyFivePoundsSixtyPence * 3) / 18
      require variableAmount == 155.93#GBP

      multiplier <- nineHundredAndThirtyFivePoundsSixtyPence / variableAmount
      //A Money / Money ratio is a Float - 6.0001282627 - so bound it, never == it
      require multiplier > 6.0001 and multiplier < 6.0002

      variableAmount += 4.07#GBP
      require variableAmount == 160.00#GBP

      variableAmount *= 0.666
      require variableAmount == 106.56#GBP

      variableAmount -= 0.56#GBP
      require variableAmount == 106.00#GBP

      variableAmount /= 4
      require variableAmount == 26.50#GBP

      variableAmount := -variableAmount
      require variableAmount == -26.50#GBP

      variableAmount := abs variableAmount
      require variableAmount == 26.50#GBP

      variableAmount := sqrt variableAmount
      require variableAmount == 5.15#GBP

      variableAmount := variableAmount ^ 6
      require variableAmount == 18657.07#GBP

      //Division by zero result is not set
      variableAmount := variableAmount/0
      require ~variableAmount?

      //Use of two difference currencies together - result is not set
      variableAmount := tenPounds + thirtyDollarsTwentyCents
      require ~variableAmount?

      //Example if pipeline to add amounts together
      amounts as List of Money := [tenPounds, nintyNinePoundsFiftyOnePence, fourtyNinePoundsSeventySixPence]
      total <- cat amounts | collect as Money
      require total == 159.27#GBP

      //Provide the exchange rate and convert to USD
      totalInUSD <- total.convert(1.32845, "USD")
      require totalInUSD == 211.58#USD

total <- cat [tenPounds, ninetyNinePoundsFiftyOnePence, fortyNinePoundsSeventySixPence] | collect as Money

A pipeline takes its values from a collection, so to stream several Money values put them in a List and cat the list, as above. Money, like the other single-value types (Integer, Float, Date ...), is not itself a pipeline source. String and Bits are iterable, over their Characters and Booleans; to stream a single value of any type, use a List or an Optional.

Locale

A Locale outputs values in localised form. Its general form is {language}_{country}, for example en_GB or de_DE. These types can be localised out of the box, and Time, Date, DateTime and Money have short, medium, long and full formats:

To localise 'Text' for end users, see Text Properties. The Locale and the Text construct together are the basis for output in a range of spoken languages.

The example formats the same data in several locales and formats, so you can see the differences:

#!ek9
defines module site.builtInTypes.locale

  defines program

    ShowLocale()

      enGB <- Locale("en_GB")
      //Note you can use underscore or dash as the separator.
      enUS <- Locale("en-US")
      deutsch <- Locale("de_DE")
      skSK <- Locale("sk", "SK")

      showIntegerFormats(enGB, deutsch, skSK)
      showFloatFormats(enGB, deutsch, skSK)
      showTimeAndDateFormats(enGB: enGB, enUS: enUS, deutsch: deutsch, skSK: skSK)
      showMoneyFormats(enGB, deutsch, skSK)

  defines function

    showIntegerFormats()
      ->
        enGB as Locale
        deutsch as Locale
        skSK as Locale

      i1 <- -92208
      i2 <- 675807

      presentation <- enGB.format(i1)
      require presentation == "-92,208"
      presentation := enGB.format(i2)
      require presentation == "675,807"

      presentation := deutsch.format(i1)
      require presentation == "-92.208"
      presentation := deutsch.format(i2)
      require presentation == "675.807"

      presentation := skSK.format(i1)
      require presentation == "-92 208"
      presentation := skSK.format(i2)
      require presentation == "675 807"

    showFloatFormats()
      ->
        enGB as Locale
        deutsch as Locale
        skSK as Locale

      f1 <- -4.9E-22
      f2 <- -1.797693134862395E12

      presentation <- enGB.format(f1)
      require presentation == "-0.00000000000000000000049"
      presentation := enGB.format(f2)
      require presentation == "-1,797,693,134,862.395"

      //With control over number of decimal places displayed
      presentation := enGB.format(f1, 22)
      require presentation == "-0.0000000000000000000005"
      presentation := enGB.format(f2, 1)
      require presentation == "-1,797,693,134,862.4"

      presentation := deutsch.format(f1)
      require presentation == "-0,00000000000000000000049"
      presentation := deutsch.format(f2)
      require presentation == "-1.797.693.134.862,395"

      presentation := skSK.format(f1)
      require presentation == "-0,00000000000000000000049"
      presentation := skSK.format(f2)
      require presentation == "-1 797 693 134 862,395"

    showTimeAndDateFormats()
      ->
        enGB as Locale
        enUS as Locale
        deutsch as Locale
        skSK as Locale

      time1 <- 12:00:01

      presentation <- enGB.shortFormat(time1)
      require presentation == "12:00"

      presentation := deutsch.mediumFormat(time1)
      require presentation == "12:00:01"

      date1 <- 2020-10-03

      presentation := enGB.shortFormat(date1)
      require presentation == "03/10/2020"

      presentation := enUS.mediumFormat(date1)
      require presentation == "Oct 3, 2020"

      presentation := skSK.longFormat(date1)
      require presentation == "3. októbra 2020"

      presentation := deutsch.fullFormat(date1)
      require presentation == "Samstag, 3. Oktober 2020"

      dateTime1 <- 2020-10-03T12:00:00Z

      presentation := enUS.shortFormat(dateTime1)
      require presentation == "10/3/20, 12:00 PM"

      presentation := enGB.mediumFormat(dateTime1)
      require presentation == "3 Oct 2020, 12:00:00"

      presentation := deutsch.longFormat(dateTime1)
      require presentation == "3. Oktober 2020, 12:00:00 Z"

      presentation := skSK.fullFormat(dateTime1)
      require presentation == "sobota 3. októbra 2020, 12:00:00 Z"

    showMoneyFormats()
      ->
        enGB as Locale
        deutsch as Locale
        skSK as Locale

      thousandsOfChileanCurrency <- 6798.9288#CLF
      tenPounds <- 10#GBP
      thirtyDollarsEightyNineCents <- 30.89#USD

      presentation <- enGB.format(tenPounds)
      require presentation == "£10.00"
      presentation := enGB.format(thirtyDollarsEightyNineCents)
      require presentation == "US$30.89"
      presentation := enGB.format(thousandsOfChileanCurrency)
      require presentation == "CLF6,798.9288"

      presentation := deutsch.format(tenPounds)
      require presentation == "10,00 £"
      presentation := deutsch.format(thirtyDollarsEightyNineCents)
      require presentation == "30,89 $"
      presentation := deutsch.format(thousandsOfChileanCurrency)
      require presentation == "6.798,9288 CLF"

      //Without the currency symbol
      presentation := deutsch.longFormat(tenPounds)
      require presentation == "10,00"

      presentation := deutsch.longFormat(thirtyDollarsEightyNineCents)
      require presentation == "30,89"

      presentation := deutsch.longFormat(thousandsOfChileanCurrency)
      require presentation == "6.798,9288"

      presentation := skSK.format(tenPounds)
      require presentation == "10,00 GBP"
      presentation := skSK.format(thirtyDollarsEightyNineCents)
      require presentation == "30,89 USD"
      presentation := skSK.format(thousandsOfChileanCurrency)
      require presentation == "6 798,9288 CLF"

      presentation := enGB.mediumFormat(tenPounds)
      require presentation == "£10"
      presentation := enGB.mediumFormat(thirtyDollarsEightyNineCents)
      require presentation == "US$31"
      presentation := enGB.format(money: thousandsOfChileanCurrency, showSymbol: true, showFractionalPart: false)
      require presentation == "CLF6,799"

      presentation := enGB.shortFormat(tenPounds)
      require presentation == "10"
      presentation := enGB.shortFormat(thirtyDollarsEightyNineCents)
      require presentation == "31"
      presentation := enGB.format(money: thousandsOfChileanCurrency, showSymbol: false, showFractionalPart: false)
      require presentation == "6,799"

It is long, but it shows the range of what you can localise and how to choose short, medium, long or full for each type.

Colour

Colour has colour manipulation built in. You can work with the Bits of a Colour to make new colours, add and subtract colours, and change transparency, saturation and lightness.

#!ek9
defines module site.builtInTypes.colour

  defines program

    ShowSimpleColour()

      //Colour can hold RGB or ARGB (i.e with alpha channel)
      //Here shown with alpha channel fully opaque
      testColour <- #FF186276
      testColourAsBits <- testColour.bits()
      require testColourAsBits == 0b11111111000110000110001001110110

      //Bitwise manipulation and colour recreation from bits
      modifiedBits <- testColourAsBits and 0b10110111000100000110001010110110
      modifiedColour <- Colour(modifiedBits)
      require modifiedColour == #B7106236
      require modifiedBits == 0b10110111000100000110001000110110

      //Note it is also possible to access the HSL values of the colour
      require modifiedColour.hue() == 148
      require modifiedColour.saturation() == 71.9298245614035
      require modifiedColour.lightness() == 22.35294117647059

      //Alter the alpha channel to control how transparent/opaque
      moreOpaqueColour <- modifiedColour.withOpaque(80)
      require moreOpaqueColour == #CC106236

      //It can be made lighter - much lighter in this case
      lighterColour <- moreOpaqueColour.withLightness(80)
      require lighterColour == #CCA7F1CA

      //Then we can saturate it more
      moreSaturatedColour <- lighterColour.withSaturation(90)
      require moreSaturatedColour == #CC9EFAC9

      //Remove some Red from this colour: '-' clears the bits given (#9E with #3A cleared is #84)
      lessRedColour <- moreSaturatedColour - #3A0000
      require lessRedColour == #CC84FAC9

      //Add in some blue: '+' sets the bits given (#C9 with #1D set is #DD)
      moreBlueColour <- lessRedColour + #00001D
      require moreBlueColour == #CC84FADD

      //Available in different formats as Strings
      require moreBlueColour.RGB() == $#84FADD
      require moreBlueColour.RGBA() == $#84FADDCC
      require moreBlueColour.ARGB() == $#CC84FADD

The actual colours from example above, shown with vertical bar background.
To show opaque and transparency.

#186276FF - testColour

 

#106236B7 - modifiedColour

 

#106236CC - moreOpaqueColour

 

#A7F1CACC - lighterColour

 

#9EFAC9CC - moreSaturatedColour

 

#84FAC9CC - lessRedColour

 

#84FADDCC - moreBlueColour

 

Programs often need to change the shade and hue of a colour, lightening and darkening it. With Colour, get the current Lightness, multiply it up or down by a factor, and make the alteration to get the new Colour. A pipeline could take a stream of Colours through a range of functions to produce a darker, lighter or more muted set.

Dimension

Dimension is a simple aggregate type: a size together with a unit of measurement. Its purpose is to make the units of a calculation explicit. You could build this yourself with a class hierarchy; EK9 provides it in the language.

Inspired by CSS dimensions, it takes these units.

Absolute lengths (m, km and mile are added; they are not for a CSS context):

Relative lengths (really only for screen rendering):

An example of Dimension:

#!ek9
defines module site.builtInTypes.dimension

  defines program

    ShowDimensionType()

      dimension1 <- 1cm
      dimension2 <- 10px
      dimension3 <- 4.5em
      dimension4 <- 1.5em

      calc1 <- dimension1 * 2
      calc2 <- dimension2 / 5
      calc3 <- dimension3 + 0.6
      calc4 <- dimension3 + dimension4

      require calc1 == 2cm
      require calc2 == 2px
      require calc3 == 5.1em
      require calc4 == 6em

      //But if we divide a dimension by another dimension we get just a number
      calc5 <- dimension3 / dimension4
      require calc5 == 3

      //Normal comparison operators as you would expect
      require calc3 < calc4

      //But also the coalescing operators
      lesser <- calc3 <? calc4
      require lesser == calc3

      calc3 += 0.9
      require calc3 == calc4

      calc3++
      require calc3 > calc4

      //Checking the calculation is not actually valid
      //different types of dimension calc1 is cm and calc2 is px
      require not (calc1 <> calc2)?

      //This is a test that the comparison was valid, not the result of the comparison
      require (calc3 < calc4)?

      calc6 <- sqrt calc4
      require calc6 == 2.449489742783178em

      //Conversion is table-driven: name the target UNIT (1 mile is 1609.344 m by definition)
      eightMiles <- 8mile
      inKM <- eightMiles.convert("km")
      require inKM == 12.874752km

      require #< inKM == 12.874752
      returnJourney <- -eightMiles
      require abs returnJourney == eightMiles

      squared <- inKM ^ 2
      require squared == 165.75923906150402km

      result <- eightMiles + inKM
      require not result?

Why EK9 does it this way. Dimension is strong typing for units: only compatible units are used together, or they are converted with an explicit method call. The Mars Climate Orbiter shows why that is 'quite important'.

Path

Like JSON (next), the object/array path is built into the language, because an object graph mixing maps and arrays is so common.

A path literal starts with $?, followed by any combination of property and array addressing:

#!ek9
defines module site.builtInTypes.jsonPaths

  defines program

    ASimpleSetOfPaths()
      stdout <- Stdout()

      path1 <- $?.some.path.inc[0].array
      path2 <- $?.another[2][1].multi-dimensional.array.access_value

      simplePath <- $?.aKey
      firstArrayElement <- $?[0]
      propertyFromFirstElement <- $?[0].a-field
      stdout.println(`${path1} ${path2} ${simplePath} ${firstArrayElement} ${propertyFromFirstElement}`)

      //Not to be confused with string conversion
      anInt <- 90
      stringRepresentation <- $anInt
      stdout.println(stringRepresentation)

      //Or json conversion of a record.
      me <- CustomerDetail(firstName: "Steve", lastName: "Limb")
      jsonOfMe <- $$me
      stdout.println(jsonOfMe)

  defines record

    CustomerDetail
      firstName as String: String()
      lastName as String: String()

      CustomerDetail()
        ->
          firstName as String
          lastName as String
        this.firstName: firstName
        this.lastName: lastName

      default operator ?
      default operator $$

It prints the paths, the String of an Integer, and the JSON of a record:

$?.some.path.inc[0].array $?.another[2][1].multi-dimensional.array.access_value $?.aKey $?[0] $?[0].a-field
90
{"firstName":"Steve","lastName":"Limb"}

JSON

JSON is built into the language and supports all the JSON types:

Other EK9 types, such as Character, Date, Time, DateTime, Duration, Millisecond, Money, Colour, Dimension and Bits, become JSON Strings.

Your own records and classes convert to JSON with the $$ operator, and for most records EK9 can generate it for you (default operator $$). To convert them back from JSON, give the type either:

A stream can be poured into a JSON (cat items > target, or collect as JSON). Pouring follows the target's shape:

See pouring into JSON.

JSON is a value. Merging, pouring, combining or converting into JSON copies what it takes: +, :~:, :^:, |, :=: and $$ (of a JSON, or of anything holding one) all copy, and so do iterating a JSON and read(). Changing an operand afterwards never reaches the result, and a JSON can never come to contain itself. The one exception is get(): it is a view into its container, so merging into what get() returns changes the container too.

CSV

CSV is a first-class construct for reading and writing RFC 4180 Comma Separated Values, modelled on JSON. It handles what a naive split(',') gets wrong: quoted fields containing the separator, doubled ("") quotes, and newlines inside quoted fields. The separator is configurable, so tab- and semicolon-separated data work too.

Its API:

#!ek9
defines module site.builtInTypes.csv

  defines program

    CsvExample()
      stdout <- Stdout()

      result <- CSV().parse("name,weight\nHydrogen,1.008\nHelium,4.0026")
      if result.isOk()
        table <- result.ok()

        //Read a data row by column name; convert the cell with the target type's constructor
        helium <- table.getOrDefault(1, CSVRow())
        weight <- BigDecimal(helium.getOrDefault("weight", "0"))

        stdout.println(`Helium weighs ${weight}`)
      else if result.isError()
        stdout.println(`Bad CSV: ${result.error()}`)

It prints:

Helium weighs 4.0026

CSVRow

A CSVRow is one row of a CSV; the same type serves the header and every data row. It is more than a List of String: a data row is header-aware, so a cell can be read by column name as well as by position. Access is total, as on List: getOrDefault(key, default) never goes out of range, it yields the default.

Cells are text. For a real type, hand the cell to that type's constructor, which parses it and returns un set if the cell is not that type: Integer(row.getOrDefault(1, "0")) or BigDecimal(row.getOrDefault("weight", "0")). This works the same for every type, your own included, and is safer than dedicated typed getters: a bad cell becomes un set rather than throwing.

Regular Expression

RegEx is the last of the built-in non-collection types. It is built into the language because regular expressions are so widely used and so expressive (if complex).

Standard shorthand sequences

Quantifiers

Boundary Matchers

An example of RegEx with match, split and group; each operation can start from the RegEx or from the String.

#!ek9
defines module site.builtInTypes.regex

  defines program

    ShowRegExType()
      stdout <- Stdout()

      sixEx <- /[a-zA-Z0-9]{6}/

      require sixEx matches "arun32"
      require sixEx not matches "kkvarun32"
      require sixEx matches "JA2Uk2"
      require sixEx not matches "arun$2"

      regEx <- /[S|s]te(?:ven?|phen)/

      firstCapital <- "Steve"
      firstLower <- "steve"

      secondCapital <- "Stephen"
      secondLower <- "stephen"

      thirdCapital <- "Steven"
      thirdLower <- "steven"

      fourthCapital <- "Stephene"
      fourthLower <- "stephene"

      require firstCapital matches regEx and firstLower matches regEx
      require secondCapital matches regEx and secondLower matches regEx
      require thirdCapital matches regEx and thirdLower matches regEx

      require fourthCapital not matches regEx
      require fourthLower not matches regEx

      //String and Regex can work either way around
      require regEx matches secondCapital

      //Examples using escape of '\'

      stockExample <- "This order was placed for QT3000! OK?"
      groupEx <- /(.*?)(\d+)(.*)/
      require groupEx matches stockExample

      fractionExample <- "3/4"
      fractionEx <- /.*\/.*/
      require fractionEx matches fractionExample

      slashEx1 <- /^Some\\Thing$/
      require slashEx1 matches "Some\Thing"

      slashEx2 <- /^Some\/Thing$/
      require slashEx2 matches "Some/Thing"

      colonDelimited <- "one:two:three:four:five"
      colonRegEx <- /:/

      //You can work with RegEx first or String first
      whenSplit <- colonDelimited.split(colonRegEx)
      alsoSplit <- colonRegEx.split(colonDelimited)

      //$ on a List renders the collection with its String elements quoted - it is not a join
      require length whenSplit == 5
      require whenSplit.getOrDefault(0, "?") == "one"
      require whenSplit.getOrDefault(4, "?") == "five"
      require whenSplit == alsoSplit

      //Check on finding groups - the capture groups of the first match, in pattern order
      extractionCheck <- "This is a sample Text 1234 with numbers in between."
      extractNumbersEx <- /(.*?)(\d+)(.*)/
      matchedGroups <- extractionCheck.group(extractNumbersEx)

      //Three groups: the text before, the digits, the text after
      require length matchedGroups == 3

      justTheNumber <- cat matchedGroups | skip 1 | head 1 | collect as String
      //Convert to an Integer - if possible
      asAnInteger <- Integer(justTheNumber)
      require $justTheNumber == "1234"
      require asAnInteger == 1234

      //Check just the last two (as an example)
      lastTwo <- cat matchedGroups | tail 2 | collect as List of String
      require length lastTwo == 2

      nonExtractionCheck <- "Another sample but with no numbers in it."
      matchedGroups := nonExtractionCheck.group(extractNumbersEx)
      //Expecting this to be unset
      require not matchedGroups?

      //Safe way of accessing some result that might not be set (in this case it is not set)
      justNotTheNumber <- cat matchedGroups | skip 1 | head 1 | collect as String
      require not justNotTheNumber?

      //Named groups: ask for one by name - a String, unset when absent
      namedDate <- /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
      dueLine <- "Due 2024-06-15 at noon"
      require dueLine.group(namedDate, "year") == "2024"
      require not dueLine.group(namedDate, "hour")?

      //Replace every match, referring to the groups as $n or ${name} - a name as \${name} in backticks
      require dueLine.replace(namedDate, "$3/$2/$1") == "Due 15/06/2024 at noon"
      require dueLine.replace(namedDate, `\${day}.\${month}.\${year}`) == "Due 15.06.2024 at noon"
      stdout.println(`Replaced [${dueLine.replace(namedDate, "<date>")}]`)

Every require holds, and it prints:

Replaced [Due <date> at noon]

The example only touches on what regular expressions can do, but it shows how writing them directly in the code cuts down the escape sequences you need.

A RegEx literal (/\|/) goes to the regular expression engine exactly as written. This is the recommended approach. A RegEx built from a String goes through the String's escaping first, so a backslash meant for the engine is written twice: RegEx("\\|") is the pattern \|, the same as /\|/. Use regex literals where you can and the question never arises.

Summary

EK9 builds a wide range of types into the language. Types such as Time, Date, DateTime, Money, Dimension and Colour give you strong typing with consistent, simple semantics.

Next Steps

Many of the examples above use a collection type; the collection types section covers them.