Collection Types

This page covers the types used as collections, some of them generic/template types. A collection holds zero or more Objects of the same or compatible type.

The Types

These are the main generic types; you parameterise each with another type. The compiler treats Optional and Result specially: it checks that you have made a 'check' before you call certain methods (see the examples below).

Supporting Types

Supporting Functions

You can use these generic functions explicitly, but the compiler also infers one as the 'super' of any function whose signature matches. They classify functions, in a 'duck typed' way: the parameters, the return and whether the function is pure decide which classification a function fits. The examples below show the inference at work.

List

Some of the examples in built-in-types use the List type. A List holds zero or more Objects of a specific type, and supports:

Compiler Note: A List is always set when created, even if empty. The compiler detects redundant ? checks on freshly created lists (E08088) and redundant empty checks on lists constructed with elements (E08093) or without (E08092).

You can make a List of any type, functions included. For example, with an abstract function for collecting data from a range of sources, you can make a list of the different functions and call them through their abstract signature.

The example below creates a List of Integer in several ways. There is no '<' or '>': declare a List of X, where X is the type. Where a type has two type parameters (like Dict), group them with '(' and ')': Dict of (S, T).

#!ek9
defines module site.collectionTypes.listCreation

  defines function

    toString as pure
      -> item as Integer
      <- rtn as String: $item

    isSet as pure
      -> item as Integer
      <- rtn as Boolean: item?

  defines program

    ListCreationExample()
      stdout <- Stdout()

      //A couple of ways to create and empty List of Integer
      aList as List of Integer: List() of Integer
      bList <- List() of Integer

      //But if you have a value you want to use from the outset then use this mechanism
      //The compiler can infer the type is a List of Integer from this.
      cList <- List(1)
      stdout.println(`${aList} ${bList} ${cList}`)

      //If you have a full set of integers you know from the outset then you can use
      //[...] as a short hand for Lists (but they are not arrays and cannot be indexed with []).
      l1 as List of Integer: [ 57, 55, 26, 24, 21 ]

      //This inferred declaration is preferred and more succinct
      l2 <- [ 1, 2, 3, 4, 5, 6, 7, Integer() ]

      //List has a $ operator (i.e. convert to string) and so can just print the contents out.
      stdout.println("List l1 is " + $l1)

      //You can use interpolated Strings if you wish.
      stdout.println(`List l2 is ${l2}`)

      //Clearly if you had many items, the line length would get very long,
      //so you can declare lists like this. Also very useful if the items in the list have
      //complex constructors.
      l3 <- [
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        Integer()
        ]

      //Again you can just print out like this
      stdout.println("List l3 is " + $l3)

      //But the alternative is to use streaming pipelines
      cat l3 | filter by isSet | map with toString > stdout

It prints:

[] [] [1]
List l1 is [57, 55, 26, 24, 21]
List l2 is [1, 2, 3, 4, 5, 6, 7, unset]
List l3 is [1, 2, 3, 4, 5, 6, 7, unset]
1
2
3
4
5
6
7

The last entry of l2 and l3 was made with Integer(), so it is not set and prints as unset. To process only valid values, use a pipeline as the example does: it drops the final unset value in l3.

The next example uses functions as delegates, treated as Objects polymorphically, in Lists and pipelines. It also shows the record construct.

#!ek9
defines module site.collectionTypes.listOfFunctions

  defines record

    //Designed to hold members of the Addams family only
    Addams
      name <- String()
      dob <- Date()

      //Sample of developer constructors, the no-argument one still gets auto generated.
      Addams() as pure
        -> dateOfBirth as Date
        dob :=? Date(dateOfBirth)

      Addams() as pure
        ->
          name as String
          dateOfBirth as Date
        this.name :=? String(name)
        dob :=? Date(dateOfBirth)

      operator $ as pure
        <- rtn as String: `${name} ${dob}`

      override operator ? as pure
        <- rtn as Boolean: name? and dob?

      operator #^ as pure
        <- rtn as String: $this

  defines function

    //Just convert Addams family member to a String representation
    addamsToString() as pure
      -> person as Addams
      <- rtn as String: `DOB: ${person.dob} Name: ${person.name}`

    //Polymorphic abstract function for getting Addams family members
    getAddams() as pure abstract
      <- rtn as Addams?

    //Using the full constructor.
    getMorticia() is getAddams as pure
      <- rtn as Addams: Addams("Morticia", 1965-01-03)

    //Use developer defined constructor then set the name
    getGomez() is getAddams as pure
      <- rtn as Addams: Addams(1963-06-08)
      rtn.name :=? "Gomez"

  //Concept to hold functions that can get Addams family members
  //Then when the time is right actually call the function to get the member.
  defines program

    ListFunctionIteratorExample()
      stdout <- Stdout()

      //Note this is a List of getAddams (function)
      //EK9 is smart enough to see they both have a common super (function signature)
      functionList <- [ getGomez, getMorticia ]
      iter <- functionList.iterator()

      //A bit long winded - see next stream pipeline example
      while iter?
        fn <- iter.next()
        //Now make the call and convert the returned value to a String for output
        stdout.println($fn())
        //Because Addams has the promote operator #^ to a String we could just do this
        stdout.println(fn())

    ListFunctionStreamExample()
      stdout <- Stdout()

      //Again a list of functions
      functionList <- [ getGomez, getMorticia ]

      //But here we pipe through to 'call' the function and get the return value
      //We use the fact that promotion can be used to convert to a default String
      cat functionList | call > stdout

      //Now if the calls to get members of the Addams family via the function
      //had to make remote or slow calls, you might be better off calling them concurrently
      //in an asynchronous manner - note the pipeline will wait until all the calls have completed.
      //The calls complete in any order, so sort them if the order matters.
      //But also in this case we want to control to conversion to a String by using a function.
      cat functionList | async | map with addamsToString | sort > stdout

      //It is also possible to just use this, rather than creating a variable to reference the list
      cat [ getGomez, getMorticia ] | async | map with addamsToString | sort > stdout

    ListFunctionsInShort()
      stdout <- Stdout()
      functionList <- [ getGomez, getMorticia ]

      iter <- functionList.iterator()
      while iter?
        fn <- iter.next()
        stdout.println(fn())

      cat functionList | call | map with addamsToString > stdout

      cat functionList | call > stdout

'ListFunctionStreamExample' prints the following. The async calls complete in any order, which is why each of those pipelines is sorted before it is output:

Gomez 1963-06-08
Morticia 1965-01-03
DOB: 1963-06-08 Name: Gomez
DOB: 1965-01-03 Name: Morticia
DOB: 1963-06-08 Name: Gomez
DOB: 1965-01-03 Name: Morticia

Using functions as delegates, in a polymorphic, Object-Oriented way, means that with pipelines you can defer processing and call functions asynchronously.

This mix of a functional approach with an Object-Oriented one matters in EK9. With type inference, generics and pipelines, the processing becomes more declarative: ListFunctionIteratorExample is more procedural than ListFunctionStreamExample. As always with a declarative approach, it takes a little more thought to write.

There are no Promises, no awaits and no call back hell.

Here are those two example bodies in short, without the comments (the full source has them as a third program).

iter <- functionList.iterator()
while iter?
  fn <- iter.next()
  stdout.println(fn())

cat functionList | call | map with addamsToString > stdout

cat functionList | call > stdout

Why EK9 does it this way. The function addamsToString still has to be written (unless the type provides the promotion operator). Following the approach, EK9 accepts that overhead: being able to change how the String is output keeps the code maintainable and easy to improve.

Operators and functions can alter and transform aggregate data as it passes through the pipeline, so you choose where and how to process it.

Set

A Set holds zero or more unique values of the same or compatible type; adding a value that is already present has no effect. Use it for de-duplication and fast membership testing (contains). Like List and Dict, a Set is insertion-ordered: iterating it yields its values in the order they were first added. That order is deterministic and reproducible across runs and JVMs, which keeps output and tests stable. Uniqueness is by value, using the type's '==' and '#?' (hashcode) operators.

It supports:

Compiler Note: A Set is always set when created, even if empty (an empty Set is a valid, usable value - not unset).

#!ek9
defines module site.collectionTypes.set

  defines program

    SetCreationExample()
      stdout <- Stdout()

      //An empty Set of String, then a Set created with a value
      names as Set of String: Set()
      seen <- Set("Alpha")

      //Adding a duplicate has no effect; iteration keeps first-insertion order
      names += "Zeta"
      names += "Alpha"
      names += "Zeta"

      //Membership test and unique count
      stdout.println("Contains Zeta? " + $names.contains("Zeta"))
      stdout.println("Unique count: " + $length names)
      stdout.println("Set names is " + $names)
      stdout.println("Set seen is " + $seen)

It prints:

Contains Zeta? true
Unique count: 2
Set names is {"Zeta", "Alpha"}
Set seen is {"Alpha"}

Optional

Each type can define what isSet means with the '?' operator. Sometimes you also need a wrapper that may or may not hold an object of a type: that is Optional.

Compiler Note: Optional(value) is always set and non-empty, while Optional() of T is always unset and empty. The compiler detects redundant ? checks (E08088/E08089) and redundant empty checks (E08092/E08093) on freshly constructed Optionals.

The example below uses Optional with Integer, but it works with any type, including a function delegate or a record. It shows Optional with other types, and how List and pipelines with flatten extract the values out of the Optional safely.

Why EK9 does it this way. Optional has a get() method, but the example does not use it, to encourage the safer mechanisms: flatten and iterator() give you the value only if it is held. Called on an empty Optional, get() throws an Exception, so the compiler only lets you call it where a check has proved the Optional holds a value (E08030).

Optional, flatten and pipelining blend several ideas pragmatically: monads from Haskell, 'pipes' from Unix, and ideas from other mainstream languages.

#!ek9
defines module site.collectionTypes.optional

  defines program

    ShowOptionalType()
      stdout <- Stdout()

      //Example with a fully qualified declaration
      maybeInteger as Optional of Integer: Optional() of Integer

      //Nothing will be output
      cat maybeInteger > stdout

      //A type inferred declaration
      item1 <- Optional(5)
      cat item1 > stdout

      //two different ways of checking if optional has a value
      require item1.iterator().hasNext()
      require item1?

      //read as List of (Optional of Integer)
      //fully qualified declaration
      emptyOptionals as List of Optional of Integer: List() of Optional of Integer
      require emptyOptionals is empty

      //type inference mechanism - much more terse
      aListOfOptionals <- List(maybeInteger)
      aListOfOptionals += item1
      aListOfOptionals += Optional(14)

      //But could have been defined like this
      someOptionals <- [maybeInteger, item1, Optional(14)]
      require length someOptionals == 3

      //Use of flatten to get the items out of the optional
      cat aListOfOptionals | flatten > stdout

      //Calculate the sum of the integers.
      sum <- cat aListOfOptionals | flatten | collect as Integer
      require sum == 19

      //Also possible to use an iterator if you wish

      while iter <- item1.iterator() then iter?
        stdout.println(`Value [${iter.next()}]`)

It prints:

5
5
14
Value [5]

The example also shows the Haskell-like (monad-ish) flatten and maybe nature of Lists and Optionals: a safe way to pull data out of such wrappers. The alternative is a range of loops and isSet checks; the pipeline reduces the cognitive load.

The next examples use other methods of Optional. The compiler checks that get() is called only within a 'safe block'; outside one it is an error (E08030).

#!ek9
defines module site.collectionTypes.optionalAccess

  defines constant
    sampleName <- "Steve"

  defines function

    someAcceptor()
      -> text as String
      require text?

    someConsumer() as pure
      -> text as String
      require text?

    testSimpleValidIfCheck1()
      o <- Optional(sampleName)

      //This use of 'is-set' with an 'if' makes the following block safe to call 'get()'
      if o?
        name <- o.get()
        require name?

    testUsingConsumer() as pure
      o <- Optional(sampleName)

      //Within a pure context a 'Consumer of T' is inferred for the function 'someConsumer'
      o.whenPresent(someConsumer)

    testUsingAcceptor()
      o <- Optional(sampleName)

      //Within a none-pure context an 'Acceptor of T' is inferred for the function 'someAcceptor'
      o.whenPresent(someAcceptor)

    testUsingTernary() as pure
      o <- Optional(sampleName)

      //This use of 'is-set' with a ternary control makes the following block safe to call 'get()'
      name <- o? <- o.get() else String()
      require name?

sum <- cat aListOfOptionals | flatten | collect as Integer

This part of the example combines the generic collection types with pipelines and the built-in syntax of cat, filter, map, collect. It works as a reduce operation that sums the valid Integers flowing through the pipeline, allowing for an empty list and for optionals that hold no value.

Iterator

The List and Optional examples above have shown the Iterator. It steps through zero or more items held in a collection. Its main methods are '?', hasNext() and next(). iter? is terser; iter.hasNext() is more readable.

The compiler checks that each call to next() is in a scope where 'is-set' has been checked (E08030). This is only a basic check: if you call next() twice, the compiler does not detect it, and the second call throws an Exception at run time when there is no next item.

Bits

Built-in-types covers the Bits type; you can view it as an ordered list of Booleans.

String

Built-in-types covers the String type; you can view it as an ordered list of Characters.

Result

Result is like Optional, but takes two types: the valid result type and the error result type. The compiler checks that you have checked the result before you use its values.

Compiler Note: The compiler can detect tautological ? and empty checks on freshly constructed Result values, as for Optional (E08088/E08089/E08092/E08093).

#!ek9
defines module site.collectionTypes.result

  defines constant
    sampleName <- "Steve"

  defines function

    getResult1() as pure
      -> text as String
      <- rtn as Result of (String, Integer): Result(text, Integer())

    //Note that due to the function signatures the appropriate Consumer/Acceptor super function is inferred
    someAcceptor()
      -> text as String
      require text?

    someConsumer() as pure
      -> text as String
      require text?

    someErrorAcceptor()
      -> code as Integer
      require code?

    someErrorConsumer() as pure
      -> code as Integer
      require code?

    testValidUseDueToChecks1()
      r <- Result(sampleName, Integer())
      //So this should be valid as a check has been done
      if r?
        okValue <- r.ok()
        require okValue?

    testValidUseDueToChecks2()
      r <- Result(sampleName, Integer())
      //So this should be valid as a check has been done this time isOk()
      if r.isOk()
        okValue <- r.ok()
        require okValue?

    testValidUseDueToChecks3()
      r <- Result(sampleName, Integer())
      //So this should be valid as a check has been done this time isOk() via a ternary
      okValue <- r.isOk() <- r.ok() else String()
      require okValue?

      //You can also do this
      errorValue <- r.isError() <- r.error() else Integer()
      require not errorValue?

    testValidUseDueToChecks4()
      r <- Result(sampleName, Integer())
      someBoolean <- true

      okValue <- someBoolean and r? <- r.ok() else String()
      require okValue?

    testAccessResult()
      r <- getResult1(sampleName)

      //These methods can be called with specific acceptors/consumers and those will only be called if the
      //result has an 'OK' value.
      r.whenOk(someAcceptor)
      r.whenOk(someConsumer)

      //Likewise for errors.
      r.whenError(someErrorAcceptor)
      r.whenError(someErrorConsumer)

      //Now you can also check if the result is in error or ok like this
      require r.isOk() or r.isError()

      //Now if you are expecting a specific OK value - you can do this.
      hasOkSteve <- r contains sampleName
      require hasOkSteve

      maybeTheOkValue <- String()
      //You can also do this quite safely - because if there is no OK value - the iterator will unset
      //So the loop won't be processed
      while iter <- r.iterator() then iter.hasNext()
        maybeTheOkValue :=: iter.next()

      //So if it was set then you'd have the value (or a copy of it in this case)
      require maybeTheOkValue?

PriorityQueue

A PriorityQueue is like a List that keeps its items in order and can be given a finite size.

Its real value is when you give it both: as the example shows, it keeps a finite number of items, in the priority order you specify.

In your comparator, a result greater than zero means the first item has the higher priority, and items are kept and output in that order. For the reverse order, take list() and reverse it.

#!ek9
defines module site.collectionTypes.priorityQueue

  defines function

    suitableLength() as pure
      -> text as String
      <- rtn as Boolean?
      minimumLength <- 5
      rtn: length of text > minimumLength

    stringCompare() as pure
      ->
        t1 as String
        t2 as String
      <-
        r as Integer: t1 <=> t2

  defines program

    ShowPriorityQueue()
      stdout <- Stdout()

      qOne <- PriorityQueue("Bill").withComparator(stringCompare)

      qOne += "And"
      qOne += "Ted"
      qOne += "Excellent"
      qOne += "Adventure"
      qOne += "Outrageous"
      qOne += "Bogus"

      //They come out in order by String comparison, not in the order added.
      allEntries <- cat qOne | collect as List of String
      stdout.println(`All entries: ${allEntries}`)

      //Now make a queue that is finite, with just 3 entries
      topThree <- qOne.withSize(3)

      //Access to a list can be done just like this
      stdout.println(`Top three: ${topThree.list()}`)

      //Missy will make the cut!
      topThree += "Missy"
      stdout.println(`After adding Missy: ${topThree.list()}`)

      //Use PriorityQueue to collect up and keep ordered as part of pipeline
      bestTwo <- PriorityQueue() of String
      bestTwo := bestTwo.withSize(2).withComparator(stringCompare)

      //Note this uses allEntries again, but filter before queueing
      cat allEntries | filter by suitableLength > bestTwo
      stdout.println(`Best two longer than five characters: ${bestTwo.list()}`)

A Comparator of String is any function taking two Strings and returning an Integer, such as 'stringCompare'; withSize gives a bounded copy of the queue. It prints:

All entries: ["Ted", "Outrageous", "Excellent", "Bogus", "Bill", "And", "Adventure"]
Top three: ["Ted", "Outrageous", "Excellent"]
After adding Missy: ["Ted", "Outrageous", "Missy"]
Best two longer than five characters: ["Outrageous", "Excellent"]

Use a PriorityQueue when you need an ordered, finite list, especially when processing a large stream of data items: as the end of a pipeline, it gathers the data into an ordered, finite collection.

To merge several PriorityQueue objects, take a List of PriorityQueue of String, flatten it in a pipeline and direct it ('>') into a PriorityQueue with the size and order you want.

Why EK9 does it this way. EK9 builds small, distinct functions and classes that combine in components, compositions and pipelines, rather than putting many capabilities inside classes.

Dict (Dictionary/Map)

The longer example below uses Dict (Dictionary/Map) and DictEntry with functions, pipelines and dynamic functions.

Compiler Note: A Dict is always set when created, even if empty. The compiler detects redundant ? checks on freshly created dicts (E08088) and redundant empty checks on dicts constructed with entries (E08093) or without (E08092).

A Dict takes two type parameters. A generic function or class with more than one parameter needs '(' ')' around them, so that the types are not ambiguous.

DictEntry (Dictionary Entry)

The example:

#!ek9
defines module site.collectionTypes.dictionary

  defines type

    List of getAddams
    Dict of (Integer, String)
    Dict of (String, Date)
    Dict of (Integer, Addams)
    Dict of (Integer, getAddams)

  defines record

    Addams
      name <- String()
      dob <- Date()

      Addams()
        -> dateOfBirth as Date
        if dateOfBirth?
          dob :=: dateOfBirth

      Addams()
        ->
          name as String
          dateOfBirth as Date
        this(dateOfBirth)
        this.name :=: name

      operator $ as pure
        <- rtn as String: `${name} ${dob}`

      override operator ? as pure
        <- rtn as Boolean: name? and dob?

      operator #^ as pure
        <- rtn as String: $this

  defines function

    idToString() as abstract
      -> id as Integer
      <- rtn as Optional of String?

    nameToDate() as abstract
      -> name as String
      <- rtn as Optional of Date?

    idToAddams() as abstract
      -> id as Integer
      <- rtn as Optional of Addams?

    idToGetAddams() as abstract
      -> id as Integer
      <- rtn as getAddams?

    addamsToString() as pure
      -> person as Addams
      <- rtn as String: `DOB: ${person.dob} Name: ${person.name}`

    getAddams() as abstract
      <- rtn as Addams?

    getMorticia() is getAddams
      <- rtn as Addams: Addams("Morticia", 1965-01-03)

    getGomez() is getAddams
      <- rtn as Addams: Addams( "Gomez", 1963-06-08)

    getPugsley() is getAddams
      <- rtn as Addams: Addams("Pugsley", 1984-10-21)

    getFester() is getAddams
      <- rtn as Addams: Addams(1960-11-21)
      rtn.name: "Fester"

    getWednesday() is getAddams
      <- rtn as Addams: Addams()
      rtn.name: "Wednesday"
      rtn.dob: 1998-01-09

  defines program

    ListDictionaryExample()
      dictionaryExample <- SimpleDictionaryUse()
      dictionaryExample.showDictionary()

  defines class

    SimpleDictionaryUse
      private getName()
        <- name String: "Gomez"

      showDictionary()
        idNameDictionary <- { 1: getName(), 2: getMorticia().name, 3: "Pugsley", 4: "Fester", 5: "Wednesday" }

        addamsParents <- {
          getName(): 1963-06-08,
          getMorticia().name: getMorticia().dob
          }

        addamsKids <- {
          "Pugsley": 1984-10-21,
          "Wednesday": 1998-01-09
          }

        nameDateDictionary <- addamsParents + addamsKids + DictEntry("Fester", 1960-11-21)

        idRecordDictionary <- {
          1: getGomez(),
          2: getMorticia(),
          3: getPugsley(),
          4: getFester(),
          5: getWednesday()
          }

        idFunctionDictionary <- {
          1: getGomez,
          2: getMorticia,
          3: getPugsley,
          4: getFester,
          5: getWednesday
          }

        results as List of String: List() of String
        keyIter <- idNameDictionary.keys()
        while keyIter?
          key <- keyIter.next()
          name <- idNameDictionary.getOrDefault(key, String())
          if name?
            results += `Key ${key} with name ${name}`

        //Some keys that won't ever be found
        invalidValues <- [9, 10, 100]

        keys1 <- idNameDictionary.keys()
        keys2 <- invalidValues.iterator()

        //This is a dynamic function that 'captures' a variable.
        nameMapping <- (idNameDictionary) is idToString as function
          rtn: Optional(idNameDictionary.getOrDefault(id, String()))

        validValues <- cat keys1, keys2 | map with nameMapping | flatten | collect as List of String

        require $validValues == `["Gomez", "Morticia", "Pugsley", "Fester", "Wednesday"]`

        //This is also a dynamic function that 'captures' a variable.
        dateMapping <- (nameDateDictionary) extends nameToDate
          rtn: Optional(nameDateDictionary.getOrDefault(name, Date()))

        //Now we've used the previous iterator up so need new ones
        //We can also use the pipeline in column format
        keys1A <- idNameDictionary.keys()
        validDates <- cat keys1A
          | map nameMapping
          | flatten
          | map dateMapping
          | flatten
          | collect as List of Date

        require $validDates == "[1963-06-08, 1965-01-03, 1984-10-21, 1960-11-21, 1998-01-09]"

        //The records and functions held in a Dict are used in the same way.
        showRecordsAndFunctions(idNameDictionary, idRecordDictionary, idFunctionDictionary)

      private showRecordsAndFunctions()
        ->
          idNameDictionary as Dict of (Integer, String)
          idRecordDictionary as Dict of (Integer, Addams)
          idFunctionDictionary as Dict of (Integer, getAddams)

        recordMapping <- (idRecordDictionary) extends idToAddams
          rtn: Optional(idRecordDictionary.getOrDefault(id, Addams()))

        //Now we've used the previous iterator up so need new ones
        keys1B <- idNameDictionary.keys()
        validAddams <- cat keys1B
          | map recordMapping
          | flatten
          | collect as List of Addams
        require length validAddams == 5

        //"[Gomez 1963-06-08, Morticia 1965-01-03, Pugsley 1984-10-21, Fester 1960-11-21, Wednesday 1998-01-09]"

        //Example of an inline dynamic function
        //And and example of cat on a call return.
        addamsFamily <- cat idNameDictionary.keys()
          | map with (idFunctionDictionary) extends idToGetAddams (rtn: idFunctionDictionary.getOrDefault(id, getGomez))
          | call
          | collect as List of Addams
        require length addamsFamily == 5

        //"[Gomez 1963-06-08, Morticia 1965-01-03, Pugsley 1984-10-21, Fester 1960-11-21, Wednesday 1998-01-09]"

        values <- cat idNameDictionary.values() | collect as List of String
        require $values == `["Gomez", "Morticia", "Pugsley", "Fester", "Wednesday"]`

        found <- idFunctionDictionary.getOrDefault(1, getGomez)
        require found?
        require $found() == "Gomez 1963-06-08"

It reuses the Addams record and the idea of abstract functions from earlier.

This part shows the shorthand for defining and populating a Dict, with keys of type Integer and values of type String.

idNameDictionary <- { 1: getName(), 2: getMorticia().name, 3: "Pugsley", 4: "Fester", 5: "Wednesday" }

For longer dictionaries, this layout suits better.

addamsKids <- {
  "Pugsley": 1984-10-21,
  "Wednesday": 1998-01-09
  }

You can also build a Dict up like this.

nameDateDictionary <- addamsParents + addamsKids + DictEntry("Fester", 1960-11-21)

idRecordDictionary and idFunctionDictionary show values that are records or function delegates. You can also iterate over the keys or the values of a dictionary. This part wraps a dictionary in a dynamic function:

nameMapping <- (idNameDictionary) is idToString as function
  rtn: Optional(idNameDictionary.getOrDefault(id, String()))

Dynamic functions come later, but this shows how a function can capture variables (here a dictionary), and how its incoming and returning variables are implicit and need not be declared. It also shows the polymorphic nature of functions: this dynamic function is the type (abstract function) idToString. It accepts the incoming parameter id and looks it up in the dictionary.

Below, two iterators are the source of Integer 'ids': the keys of the dictionary, and a List of Integer with no entries in the dictionary. This is the safe way to access and pipeline information: nameMapping returns an empty Optional for a missing id, and flatten makes sure no invalid value is collected.

validValues <- cat keys1, keys2 | map with nameMapping | flatten | collect as List of String

Standard, reusable patterns emerge with pipelines: the other pipelines in the example follow the same pattern with different types and functions. Where the type is a function delegate, the call or async command runs the function to get its return value.

Generic Functions and inference

EK9 uses the following generic functions widely, and the compiler infers them for an ordinary function that meets the signature.

The standard libraries use them for 'call back' operations: they let your components interact with libraries by producing/supplying or consuming/accepting objects and functions.

Supplier

Supplies an object of type 'T', in a pure context.

$ ek9 -h Supplier
EK9 Help for 'Supplier'
========================================


--- TYPE API (org.ek9.lang) ---

    Supplier of type T as pure abstract
      <- r as T?

Producer

Supplies an object of type 'T' in a non-pure context: making it can mutate objects.

$ ek9 -h Producer
EK9 Help for 'Producer'
========================================


--- TYPE API (org.ek9.lang) ---

    Producer of type T as abstract
      <- r as T?

Consumer

Consumes an object of type 'T' in a pure context (it cannot mutate any objects).

$ ek9 -h Consumer
EK9 Help for 'Consumer'
========================================


--- TYPE API (org.ek9.lang) ---

    Consumer of type T as pure abstract
      -> t as T

BiConsumer

Consumes objects of type 'T' and 'U' in a pure context (it cannot mutate any objects).

$ ek9 -h BiConsumer
EK9 Help for 'BiConsumer'
========================================


--- TYPE API (org.ek9.lang) ---

    BiConsumer of type (T, U) as pure abstract
      ->
        t as T
        u as U

Acceptor

Accepts an object of type 'T' in a non-pure way: it can mutate.

$ ek9 -h Acceptor
EK9 Help for 'Acceptor'
========================================


--- TYPE API (org.ek9.lang) ---

    Acceptor of type T as abstract
      -> t as T

BiAcceptor

Accepts objects of type 'T' and 'U' in a non-pure way: it can mutate.

$ ek9 -h BiAcceptor
EK9 Help for 'BiAcceptor'
========================================


--- TYPE API (org.ek9.lang) ---

    BiAcceptor of type (T, U) as abstract
      ->
        t as T
        u as U

UnaryOperator

Accepts an object of type 'T' in a pure way and returns another object of type 'T'.

$ ek9 -h UnaryOperator
EK9 Help for 'UnaryOperator'
========================================


--- TYPE API (org.ek9.lang) ---

    UnaryOperator of type T as pure abstract
      -> t as T
      <- r as T?

Function

Accepts an object of type 'T' in a pure way and returns another object of type 'R'.

$ ek9 -h Function
EK9 Help for 'Function'
========================================

--- CONSTRUCT EXAMPLES ---
Use: ek9 -qd 'classes'.
  ek9 -q 93 (define class). ek9 -q 49 (functions).
  ek9 -q 97 (class vs record). ek9 -q 106 (traits).
  ek9 -q 111 (components).

--- FUNCTION CONSTRUCT ---
First-class callable values with single-method semantics.
Functions in EK9 are types — not just code blocks.

ABSTRACT FUNCTION (signature only — unique to EK9):
(from JustAbstractFunctions.ek9)

  mathOperation() as pure abstract
    ->
      x as Float
      y as Float
    <-
      result as Float?

IMPLEMENTING ABSTRACT FUNCTIONS (use 'is' or 'extends'):
(from JustAbstractFunctions.ek9)

  add() is mathOperation as pure
    ->
      x as Float
      y as Float
    <-
      result as Float: x + y

  subtract() is mathOperation as pure
    ->
      x as Float
      y as Float
    <-
      result as Float: x - y

SINGLE-PARAMETER ABSTRACT FUNCTION:
(from JustFunctions.ek9)

  mathOperation() as pure abstract
    -> mainValue as Float
    <- result as Float?

  multiply() is mathOperation as pure
    -> mainValue as Float
    <- result as Float: mainValue * 5.6

FUNCTIONS AS VALUES (first-class):
(from JustAbstractFunctions.ek9)

  for op in [add, subtract, divide, multiply]
    stdout.println(`Result: ${op(21, 7)}`)

FUNCTION AS DELEGATE (variable holds function):
(from VerySimpleFunctions.ek9)

  funB as abstractFunction: function1
  result <- funB()

DYNAMIC FUNCTIONS (closures with capture):
(from JustAbstractFunctions.ek9)

  addFunction <- () is mathOperation as pure function
    result:=? x+y

  specialFunction <- (fixedValue) is mathOperation pure
    result:=? (x+fixedValue) * y^fixedValue

INLINE LAMBDA-STYLE (single expression in parentheses):
(from JustAbstractFunctions.ek9)

  ops <- [
    () is mathOperation as pure (result:=? x+y),
    () is mathOperation as pure (result:=? x-y),
    (fixedValue) is mathOperation pure (result:=? (x+fixedValue) * y^fixedValue)
    ]

NAMED CAPTURE PARAMETERS:
(from JustFunctions.ek9)

  inlineFunction <- (
    v1: 12,
    v2: 64,
    v3: 90
    ) is mathOperation as pure (result:=? v1 + v2 + v3 * mainValue)

CONDITIONAL FUNCTION RETURN (strategy pattern):
(from JustFunctions.ek9)

  suitableMathOperation()
    -> mainValue as Float
    <- op as mathOperation: multiply
    heavyThreshold <- 21
    op: mainValue < heavyThreshold <- heavyMultiply else multiply

KEY DIFFERENCES FROM OTHER LANGUAGES:
  - Abstract functions are STANDALONE types (not just abstract methods)
  - 'is' or 'extends' implements abstract function
  - Dynamic functions capture variables by value
  - No anonymous lambdas — all functions are named or typed
  - 'pure' modifier: 'as pure' or just 'pure' (both valid)

Use: ek9 -q 49 (functions). ek9 -q 59 (pipelines).
See: https://ek9.io/functions.html


--- TYPE API (org.ek9.lang) ---

    Function of type (T, R) as pure abstract
      -> t as T
      <- r as R?

BiFunction

Accepts objects of type 'T' and 'U' in a pure way and returns another object of type 'R'.

$ ek9 -h BiFunction
EK9 Help for 'BiFunction'
========================================


--- TYPE API (org.ek9.lang) ---

    BiFunction of type (T, U, R) as pure abstract
      ->
        t as T
        u as U
      <-
        r as R?

Predicate

Accepts an object of type 'T' in a pure way and returns a Boolean: a 'test' function.

$ ek9 -h Predicate
EK9 Help for 'Predicate'
========================================


--- TYPE API (org.ek9.lang) ---

    Predicate of type T as pure abstract
      -> t as T
      <- r as Boolean?

Assessor

Accepts an object of type 'T' in a non-pure way (it can mutate data) and returns a Boolean: a 'test' function.

$ ek9 -h Assessor
EK9 Help for 'Assessor'
========================================


--- TYPE API (org.ek9.lang) ---

    Assessor of type T as abstract
      -> t as T
      <- r as Boolean?

BiPredicate

Accepts objects of type 'T' and 'U' in a pure way and returns a Boolean: a 'test' function.

$ ek9 -h BiPredicate
EK9 Help for 'BiPredicate'
========================================


--- TYPE API (org.ek9.lang) ---

    BiPredicate of type (T, U) as pure abstract
      ->
        t as T
        u as U
      <-
        r as Boolean?

Comparator

Accepts two objects of type 'T' and returns an Integer: whether t1 is greater than, less than or equal to t2. It is 'pure', so it cannot mutate data.

$ ek9 -h Comparator
EK9 Help for 'Comparator'
========================================


--- TYPE API (org.ek9.lang) ---

    Comparator of type T as pure abstract
      ->
        t1 as T
        t2 as T
      <-
        r as Integer?

Summary

Collection types, pipelines and functions combine consistently. The pipeline syntax is designed to be separate from the methods on Objects and Collections.

Why EK9 does it this way. These function signatures are classified because they are very common patterns, and using functions this way makes software 'composable'.

Next Steps

That is the end of the collection types. Standard Types covers the classes that provide functionality in EK9's standard API. They are very likely to grow in number and capability as EK9 develops.