What are higher-order functions and why should I use them?

← Getting Started · Ref: Q56

A higher-order function is a function that either accepts a function as a parameter, returns a function as its result, or both. EK9's type system makes higher-order functions natural and type-safe because functions ARE types.

FUNCTION RETURNING A FUNCTION

A function can return a delegate based on runtime conditions:

  getProcessor()
    -> mode as String
    <- processor as Processor?
    if mode == "upper"
      processor := UpperProcessor
    else
      processor := LowerProcessor

The caller gets a function reference and calls it:

  transform <- getProcessor("upper")
  result <- transform("hello")

CONDITIONAL FUNCTION SELECTION (TERNARY)

EK9's ternary syntax is especially elegant for selecting between two functions:

  selectOperation()
    -> value as Float
    <- op as mathOperation: value < 0.0 <- addOp else subtractOp

This returns addOp when value is negative, subtractOp otherwise. One line, completely readable.

WHY USE HIGHER-ORDER FUNCTIONS?

1. STRATEGY PATTERN — Replace if/else chains with function selection:

  Instead of: if mode == "A" then doA() else if mode == "B" then doB()
  Use: processor <- getProcessor(mode) then processor(data)

2. CONFIGURABLE ALGORITHMS — Pass behaviour as a parameter:

  processList()
    -> items as List of String, transform as Processor
    <- results as List of String: List() of String
    for item in items
      results += transform(item)

3. COMPOSITION — Build complex behaviour from simple functions:

  Pipeline stages, each a function delegate, composed at runtime.

4. TESTABILITY — Mock behaviour by passing different function delegates.

In Java, the strategy pattern requires defining an interface, implementing it in separate classes, and wiring them together. In EK9, you just pass a function. In Python, you can pass functions but there is no type contract. In Rust, you need trait objects. EK9 gives you the safety of typed strategies with the simplicity of passing functions.

See Q51 for abstract function types. See Q54 for pure function constraints on strategy selection. See Q55 for function delegates. See Q57 for using higher-order functions to change class behaviour without subclassing. See Q58 for generic function templates. See Q59 for higher-order functions in stream pipelines. See Q89 for stream pipeline basics.

Example

defines module qa.higherorderfunction

  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()

    getProcessor()
      -> mode as String
      <- processor as Processor?
      if mode == "upper"
        processor := UpperProcessor
      else
        processor := LowerProcessor

    processList()
      ->
        items as List of String
        transform as Processor
      <- results as List of String: List() of String
      for item in items
        results += transform(item)

  defines program

    HigherOrderDemo()
      stdout <- Stdout()

      // Function returning a function
      upper <- getProcessor("upper")
      stdout.println(`upper: ${upper("hello")}`)

      lower <- getProcessor("lower")
      stdout.println(`lower: ${lower("HELLO")}`)

      // Function accepting a function — configurable algorithm
      words <- ["Hello", "World", "EK9"]
      uppered <- processList(words, UpperProcessor)
      for word in uppered
        stdout.println(`processed: ${word}`)

      // Strategy selection — swap behaviour at runtime
      modes <- ["upper", "lower", "upper"]
      for mode in modes
        proc <- getProcessor(mode)
        stdout.println(`${mode}: ${proc("Test")}`)

Common mistakes

E07110 — A pure function must have a body to be pure. Removing 'abstract' and adding 'pure' without a concrete body triggers E07110. See ek9 -h E07110 for details.

Incorrect:

Processor() as pure

Correct:

Processor() as abstract

E05160 — If the parent function is not pure, child functions must also not be pure. Adding 'as pure' on a child of a non-pure parent triggers E05160 — purity mismatch in type hierarchy. See ek9 -h E05160 for details.

Incorrect:

UpperProcessor() extends Processor as pure

Correct:

UpperProcessor() extends Processor
Other ways to ask this
  • How do I create a function that returns a function in EK9?
  • How do I use the strategy pattern with EK9 functions?
  • Why are higher-order functions useful in EK9?

Coming from another language?

Java: requires functional interfaces as strategy types, method references (::) or lambdas, verbose anonymous class pattern for pre-Java-8, no type hierarchy for strategies. Python: functions are first-class so higher-order works naturally, but no type contracts, no compile-time safety, duck typing means errors at runtime. JavaScript: functions are first-class, higher-order is common (callbacks, promises), but no type safety, callback hell, no quality enforcement. Rust: higher-order functions use Fn trait bounds or fn pointers, complex lifetime annotations for closures, cannot name return types without impl Trait, verbose Box<dyn Fn> for stored delegates. Go: higher-order functions work with function types, but no type hierarchies, no generics until 1.18, verbose type declarations. C#: Func<T,R> and Action<T> for higher-order patterns, delegate types, LINQ is built on higher-order functions. Kotlin: higher-order functions with structural types, inline keyword for performance, but no nominal function type hierarchies. Swift: higher-order functions with structural closure types, @escaping annotation complexity. EK9: higher-order functions are natural because functions ARE types, ternary syntax for elegant function selection, abstract function types provide compile-time contracts, no verbose interface boilerplate, complete type safety.

Keywords: beginner, return, compose, configure, delegate, first, start, order, select, conditional, migrate, intro, higher, pattern, ternary, strategy, function