How do I implement a fluent API in EK9?

← Design Patterns and Idioms · Ref: Q265

EK9 supports fluent APIs through two approaches: immutable builder classes where each method returns a new instance, and stream pipelines which are the natural fluent pattern.

IMMUTABLE BUILDER PATTERN

Each method returns a new builder instance with accumulated state:

  QueryBuilder
    query as String
    select()
      -> columns as String
      <- rtn as QueryBuilder: QueryBuilder("SELECT " + columns)
    from()
      -> table as String
      <- rtn as QueryBuilder: QueryBuilder(query + " FROM " + table)

This is immutable: each call creates a new builder rather than mutating the current one.

CHAINING CALLS

Store intermediate results and chain:

  qb <- QueryBuilder()
  qb: qb.select("name, age")
  qb: qb.from("users")
  qb: qb.where("age > 18")
  stdout.println(qb.build())

STREAM PIPELINES AS FLUENT API

EK9's stream pipelines are the idiomatic fluent pattern:

  cat numbers | filter by isEven | map with doubleIt | collect as List of Integer

This is naturally fluent: each stage passes results to the next.

WHY NOT RETURN THIS

In many languages, fluent APIs work by returning 'this' from each method. EK9 encourages immutability: methods return new instances rather than mutating and returning the same object. This makes each intermediate state independent and safe to reuse.

WHEN TO USE EACH

Use immutable builders for configuration and query construction. Use stream pipelines for data transformation. Use function composition (Q59) for processing chains.

See Q89 for list streams. See Q235 for stream operations reference. See Q237 for streams vs loops.

Example

defines module qa.patterns.fluent

  defines function

    isEven() as pure
      -> num as Integer
      <- rtn as Boolean: num mod 2 == 0

    doubleIt() as pure
      -> num as Integer
      <- rtn as Integer: num * 2

  defines class

    QueryBuilder
      query as String: String()

      default QueryBuilder()

      QueryBuilder()
        -> q as String
        this.query :=? String(q)

      select()
        -> columns as String
        <- rtn as QueryBuilder: QueryBuilder("SELECT " + columns)

      from()
        -> table as String
        <- rtn as QueryBuilder: QueryBuilder(`${query} FROM ${table}`)

      where()
        -> condition as String
        <- rtn as QueryBuilder: QueryBuilder(`${query} WHERE ${condition}`)

      build() as pure
        <- rtn as String: String(query)

      default operator ?

  defines program

    FluentApiDemo()
      stdout <- Stdout()

      // === IMMUTABLE BUILDER ===

      qb <- QueryBuilder()
      qb: qb.select("name, age")
      qb: qb.from("users")
      qb: qb.where("age > 18")
      stdout.println(qb.build())

      // === REUSABLE BASE ===

      base <- QueryBuilder().select("id, name")
      fromUsers <- base.from("users")
      fromOrders <- base.from("orders")
      stdout.println(fromUsers.build())
      stdout.println(fromOrders.build())

      // === STREAM PIPELINE: NATURAL FLUENT ===

      numbers <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      evens <- cat numbers | filter by isEven | map with doubleIt | collect as List of Integer
      stdout.println(`Doubled evens: ${evens}`)

Common mistakes

E08180 — Class fields must be initialised inline. Declaring a field without an initialiser triggers E08180 — uninitialised field. Always provide a default value. See ek9 -h E08180 for details.

Incorrect:

query as String

Correct:

query as String: String()

E50001 — Renaming the variable means later references to 'qb' become unresolved, triggering E50001. Variable names must be consistent throughout the scope. See ek9 -h E50001 for details.

Incorrect:

qbXYZ <- QueryBuilder()

Correct:

qb <- QueryBuilder()
Other ways to ask this
  • How do I chain method calls in EK9?
  • How do I create a builder with method chaining?
  • What is the EK9 approach to fluent interfaces?

Coming from another language?

Java: fluent APIs return this, StringBuilder pattern, Stream API for data. Python: method chaining returns self, list comprehensions. Rust: builder pattern with consume-and-return, iterator chains. Go: functional options pattern, no method chaining convention. Kotlin: apply/also scope functions, Sequence for lazy chains. EK9: immutable builder (each method returns new instance), stream pipelines (cat | filter | map | collect) as natural fluent API.

Keywords: builder, immutable, design, api, compose, method, stream, idiom, fluent, pipeline, chain, pattern