How do I pass functions as delegates in EK9?

← Getting Started · Ref: Q55

Because EK9 functions are types, you can store them in variables, pass them as parameters, and return them from other functions. A variable that holds a function reference is called a delegate.

STORING A FUNCTION IN A VARIABLE

Declare a variable typed as the abstract function, assign a concrete implementation:

  processor as Processor: UpperProcessor
  result <- processor("Hello")    //Calls UpperProcessor

Without parentheses, 'UpperProcessor' is a REFERENCE. With parentheses, 'UpperProcessor("Hello")' is a CALL. This distinction is critical.

REASSIGNING DELEGATES

Change the function a delegate points to at runtime:

  processor: LowerProcessor
  result <- processor("Hello")    //Now calls LowerProcessor

PASSING FUNCTIONS AS PARAMETERS

Functions can accept other functions as parameters. Use the abstract function type:

  applyToGreeting()
    -> transform as Processor
    <- result as String: transform("Hello World")

Call with any implementation:

  upper <- applyToGreeting(UpperProcessor)
  lower <- applyToGreeting(LowerProcessor)

USING BUILT-IN GENERIC FUNCTION TYPES

For common patterns, use the built-in abstract function types instead of defining your own:

  Consumer of T       //Pure: takes T, returns nothing
  Acceptor of T       //Not pure: takes T, returns nothing
  Predicate of T      //Pure: takes T, returns Boolean
  UnaryOperator of T  //Pure: takes T, returns T
  Supplier of T       //Pure: takes nothing, returns T
  Comparator of T     //Pure: compares two T values

These save you from defining abstract functions for standard patterns. See Q54 for the pure/non-pure distinction.

FUNCTIONS IN COLLECTIONS

Store delegates in a list using the idiomatic literal syntax:

  processors <- [UpperProcessor, LowerProcessor]
  for proc in processors
    stdout.println(proc("Hello"))

This creates a List of Processor inferred from the contents. Much cleaner than explicit List construction.

See Q51 for abstract functions. See Q56 for higher-order functions that return delegates. See Q57 for using delegates to change class behaviour. See Q59 for dynamic functions as pipeline stage delegates. See Q89 for using function delegates in stream pipelines. See Q109 for composition using delegation. See Q235 for complete stream operations reference. See Q262 for observer pattern using function delegates as event listeners.

Example

defines module qa.functiondelegate

  defines function

    Processor() as abstract
      -> text as String
      <- result as String?

    UpperProcessor() extends Processor
      -> text as String
      <- result as String: text.upperCase()

    LowerProcessor() extends Processor
      -> text as String
      <- result as String: text.lowerCase()

    applyToGreeting()
      -> transform as Processor
      <- result as String: transform("Hello World")

  defines program

    DelegateDemo()
      stdout <- Stdout()

      // Direct call — with parentheses
      stdout.println(`direct: ${UpperProcessor("Hello")}`)

      // Delegate — variable holds function reference
      processor as Processor: UpperProcessor
      stdout.println(`delegate: ${processor("Hello")}`)

      // Reassign delegate
      processor: LowerProcessor
      stdout.println(`reassigned: ${processor("Hello")}`)

      // Pass function as parameter
      upper <- applyToGreeting(UpperProcessor)
      lower <- applyToGreeting(LowerProcessor)
      stdout.println(`upper: ${upper}`)
      stdout.println(`lower: ${lower}`)

      // Functions in a list — idiomatic literal syntax
      processors <- [UpperProcessor, LowerProcessor]
      for proc in processors
        stdout.println(`list: ${proc("World")}`)

Common mistakes

E50030 — Without 'extends Processor', UpperProcessor is a standalone function with no type relationship to Processor. Assigning it to a Processor variable fails because the types are incompatible. Use 'extends' or 'is' to establish the function hierarchy. See ek9 -h E50030 for details.

Incorrect:

UpperProcessor() as pure

Correct:

UpperProcessor() extends Processor
Other ways to ask this
  • How do function delegates work in EK9?
  • How do I store a function in a variable in EK9?
  • How do I pass a function as a parameter in EK9?

Coming from another language?

Java: functional interfaces as parameter types, method references with :: syntax, no first-class function variables (only interface references), lambda-to-interface conversion is implicit structural matching. Python: functions are first-class values, pass by name, no type contract on parameters, duck typing. JavaScript: functions are first-class, pass by reference, no type safety, callback hell pattern. Rust: fn pointers for simple functions, Fn/FnMut/FnOnce trait objects for closures, dyn Fn for dynamic dispatch, Box<dyn Fn> for ownership, complex lifetime annotations. Go: function values, pass by name, no type hierarchies, limited type safety. C#: delegates and Func/Action types, explicit delegate declaration needed, += multicast delegates add complexity. Kotlin: function types (String) -> String are structural, no nominal type identity, SAM conversion for interfaces. Swift: function types (String) -> String are structural, @escaping for stored closures. EK9: functions ARE types, store in variables typed as abstract function, pass as parameters using abstract function type, collect in lists with literal syntax, delegate vs call distinguished by parentheses, built-in generic function types for common patterns.

Keywords: beginner, list, store, delegate, first, variable, pass, parameter, predicate, start, reference, consumer, collection, intro, call, parentheses, function