Implement me a strategy pattern for applying different discount calculations to a price, using an abstract function and two dynamic function strategies.

← Design Patterns and Idioms · Ref: Q1238

EK9 implements the strategy pattern using an abstract function as the strategy interface and dynamic functions as concrete strategies. A separate function takes the value AND the strategy as parameters, calling the strategy through the function-call syntax.

ABSTRACT FUNCTION AS STRATEGY

  Discounter() as pure abstract
    -> price as Float
    <- rtn as Float?

DYNAMIC FUNCTION STRATEGIES

Each concrete strategy is a dynamic function with the signature of the abstract:

  tenPercent <- () is Discounter as pure function
    rtn: price * 0.9
  twentyPercent <- () is Discounter as pure function
    rtn: price * 0.8

The '() is Discounter as pure function' form creates an instance of the abstract function with a concrete body. The body uses 'rtn:' to assign the return variable.

PURE FUNCTION THAT TAKES A STRATEGY

The strategy is passed as a parameter typed by the abstract function:

  applyDiscount() as pure
    ->
      price as Float
      strategy as Discounter
    <- rtn as Float: strategy(price)

Calling 'strategy(price)' invokes whichever concrete strategy was passed in.

USAGE

  full <- 100.0
  cheaper <- applyDiscount(full, tenPercent)
  cheapest <- applyDiscount(full, twentyPercent)
  stdout.println(`Full: ${full}, 10% off: ${cheaper}, 20% off: ${cheapest}`)

KEY ADVANTAGES

No class hierarchy needed. The abstract function defines the contract; dynamic functions provide implementations; the consumer just calls the parameter as a function. This is more concise than the Java/C# strategy interface + class implementations approach.

See Q214 for strategy pattern basics. See Q57 for strategy without subclassing. See Q52 for dynamic functions.

Example

defines module qa.patterns.discountstrategy

  defines function

    Discounter() as pure abstract
      -> price as Float
      <- rtn as Float?

    applyDiscount() as pure
      ->
        price as Float
        strategy as Discounter
      <- rtn as Float: strategy(price)

  defines program

    DiscountStrategyDemo()
      stdout <- Stdout()

      tenPercent <- () is Discounter as pure function
        rtn: price * 0.9

      twentyPercent <- () is Discounter as pure function
        rtn: price * 0.8

      full <- 100.0
      cheaper <- applyDiscount(full, tenPercent)
      cheapest <- applyDiscount(full, twentyPercent)

      stdout.println(`Full: ${full}, 10% off: ${cheaper}, 20% off: ${cheapest}`)

Common mistakes

E07110 — An abstract function with no body must be declared 'as abstract' (or 'as pure abstract'). Using 'as open' is for functions WITH a body that can be overridden. See ek9 -h E07110 for details.

Incorrect:

Discounter() as open

Correct:

Discounter() as pure abstract
Other ways to ask this
  • Create a Discounter strategy with two implementations and a function that applies the chosen strategy.
  • Show me how to swap discount algorithms at runtime using EK9 abstract functions.
  • Write a price calculator that takes a discount strategy as a parameter.
  • Build a strategy pattern example for ten-percent and twenty-percent discounts.

Coming from another language?

Java: Strategy interface with implementing classes (DiscountStrategy interface, TenPercentDiscount implements). Python: pass functions directly (first-class). Kotlin: function types ((Double) -> Double) or interface implementations. Rust: trait objects (Box<dyn Discounter>) or closures. Go: function types. EK9: abstract function as the contract and dynamic functions as implementations — more concise than interface+class but with explicit type contract.

Keywords: callback, abstract function, behaviour parameterisation, strategy, swap algorithm, discount, pattern, dynamic function