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.
- Supplier
- Producer
- Consumer
- BiConsumer
- Acceptor
- BiAcceptor
- UnaryOperator
- Function
- BiFunction
- Predicate
- Assessor
- BiPredicate
- Comparator
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:
- empty - is the List empty ie has no contents
- length - how many items does the List have
- getOrDefault - get item at index with a safe default if index is out of range
- reverse - reverse the order in the list ('~')
- contains - See comparison for details
- '+' - See add for details
- '+=' - See add assignment for details
- '-' - See subtract for details
- '-=' - See subtract assignment for details
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
Full source: collectionTypes_list_of_functions.ek9 - Download collectionTypes_list_of_functions.ek9
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:
- empty - is the Set empty ie has no contents
- length - how many unique items does the Set have
- contains - is a value a member of the Set (See comparison)
- '+' - add a value, or form the union with another Set (See add)
- '+=' - add a value / union in place (See add assignment)
- '-' - remove a value, or form the difference with another Set (See subtract)
- '-=' - remove a value / difference in place (See subtract assignment)
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?
Download collectionTypes_optional_access.ek9
sum <- cat aListOfOptionals | flatten | collect as Integer
Full source: collectionTypes_optional.ek9 - Download collectionTypes_optional.ek9
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?
Download collectionTypes_result.ek9
PriorityQueue
A PriorityQueue is like a List that keeps its items in order and can be given a finite size.
- Ordering. Give it a comparator with withComparator. Without one, it orders the items by their own '<=>' operator, not in the order they were added.
- Size. Without a size, every item added stays in the queue. With a size (withSize), the number of items stays at or below that size: the lowest priority items are removed.
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" }
Full source: collectionTypes_dictionary.ek9 - Download collectionTypes_dictionary.ek9
For longer dictionaries, this layout suits better.
addamsKids <- { "Pugsley": 1984-10-21, "Wednesday": 1998-01-09 }
Full source: collectionTypes_dictionary.ek9 - Download collectionTypes_dictionary.ek9
You can also build a Dict up like this.
nameDateDictionary <- addamsParents + addamsKids + DictEntry("Fester", 1960-11-21)
Full source: collectionTypes_dictionary.ek9 - Download collectionTypes_dictionary.ek9
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()))
Full source: collectionTypes_dictionary.ek9 - Download collectionTypes_dictionary.ek9
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
Full source: collectionTypes_dictionary.ek9 - Download collectionTypes_dictionary.ek9
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.