Can functions form multi-level inheritance chains?

← Function Extension · Ref: Q593

EK9 functions can form multi-level chains: an abstract function at the top, with concrete implementations at each level. Functions are closed by default, like classes: a concrete function can only be extended when it is declared 'as open'.

A MULTI-LEVEL CHAIN

An abstract function can extend another abstract function, and a concrete function at the end of the chain implements it:

  defines function
    BaseOp as pure abstract
      -> x as Integer
      <- rtn as Integer?
    PositiveOp is BaseOp as pure abstract
      -> x as Integer
      <- rtn as Integer?
    DoubleOp is PositiveOp as pure
      -> x as Integer
      <- rtn as Integer: x + x

CONCRETE FUNCTIONS ARE CLOSED BY DEFAULT

A function that provides an implementation cannot be extended unless it is declared 'as open' (for example DoubleOp is PositiveOp as pure open). Extending a closed function is error E05030, not open to be extended - the same rule as classes.

MULTIPLE IMPLEMENTATIONS

An abstract function can have many direct implementations, creating a flat hierarchy with one abstract parent and multiple concrete children.

See Q588 for function extension basics. See Q589 for multiple implementations. See Q594 for extension rules. See Q595 for pure abstract function constraints.

Example

defines module qa.function.multilevelchain

  defines constant

    MIN_FILTER_LENGTH <- 3

  defines function

    //Abstract function: base of hierarchy
    Filter as pure abstract
      -> input as String
      <- passes as Boolean?

    //A second abstract level: extends Filter and is itself extended below
    MinLengthFilter is Filter as pure abstract
      -> input as String
      <- passes as Boolean?

    //Multiple direct implementations (flat hierarchy)
    NonEmptyFilter is Filter as pure
      -> input as String
      <- passes as Boolean: length input > 0

    //LongEnoughFilter is a concrete function three levels from the top of this chain
    LongEnoughFilter is MinLengthFilter as pure
      -> input as String
      <- passes as Boolean: length input > MIN_FILTER_LENGTH

    ContainsAtFilter is Filter as pure
      -> input as String
      <- passes as Boolean: input contains "@"

  defines program

    MultilevelFunctionChainDemo()
      stdout <- Stdout()

      //All implementations share the abstract type
      filters <- List() of Filter
      filters += NonEmptyFilter
      filters += LongEnoughFilter
      filters += ContainsAtFilter

      testValues <- ["", "hi", "hello", "user@host"]

      for testVal in testValues
        stdout.println(`Testing: "${testVal}"`)
        for filterFn in filters
          stdout.println(`  Passes: ${filterFn(testVal)}`)

Common mistakes

E08120 — A function declared as pure must not mutate variables. The += operator mutates input in place, so the compiler reports E08120. See ek9 -h E08120 for details.

Incorrect:

NonEmptyFilter is Filter as pure
      -> input as String
      <- passes as Boolean: length input > 0
      input += "x"

Correct:

NonEmptyFilter is Filter as pure
      -> input as String
      <- passes as Boolean: length input > 0
Other ways to ask this
  • Can a concrete function be extended further?
  • How deep can function hierarchies go?
  • Can I have Abstract -> Middle -> Concrete function chains?

Coming from another language?

Java: functional interfaces are flat (one method). Kotlin: function types are structural. Rust: Fn traits have no hierarchy. EK9: abstract functions can have multiple concrete implementations, concrete functions are closed unless declared as open.

Keywords: chain, extension, multi-level, function, extend, hierarchy, abstract, closed, concrete