How do I use BiFunction and BiRoutine for two-input transformations?

← Generics · Ref: Q713

BiFunction and BiRoutine are the three-type-parameter transformation types. They take inputs 't' of type T and 'u' of type U, and return 'r' of type R.

BIFUNCTION IS PURE

BiFunction of (T, U, R) takes two parameters and returns a result. Being pure, the transformation is deterministic:

  combiner <- () is BiFunction of (String, Integer, String) as pure function
    r: `${t}: ${u}`

BIROUTINE IS IMPURE

BiRoutine of (T, U, R) has the same signature but is NOT pure:

  logCombiner <- () is BiRoutine of (String, Integer, String) as function
    stdout <- Stdout()
    stdout.println(`Combining: ${t} with ${u}`)
    r: `${t}-${u}`

COMBINER PATTERN

BiFunction is ideal for combining two values into one:

  merge() as pure
    -> a as String, b as Integer, combiner as BiFunction of (String, Integer, String)
    <- result as String: combiner(a, b)

See Q54 for pure vs impure concepts. See Q710 for single-input Function/Routine.

Example

defines module qa.genericsdeep.bifunctionbiroutine

  defines function

    <?-
      Helper that applies a pure BiFunction combiner.
    -?>
    merge() as pure
      ->
        name as String
        count as Integer
        combiner as BiFunction of (String, Integer, String)
      <- result as String: combiner(name, count)

    <?-
      Helper that applies an impure BiRoutine combiner.
    -?>
    mergeWithEffects()
      ->
        name as String
        count as Integer
        combiner as BiRoutine of (String, Integer, String)
      <- result as String: combiner(name, count)

  defines program

    BiFunctionBiRoutineDemo()
      stdout <- Stdout()

      //BiFunction: pure, deterministic two-input transform
      //Body uses 't' and 'u' for inputs, 'r' for return
      combiner <- () is BiFunction of (String, Integer, String) as pure function
        r: `${t}: ${u}`

      stdout.println(`Combined: ${merge("Alice", 42, combiner)}`)

      //BiFunction with Boolean result
      checker <- () is BiFunction of (String, Integer, Boolean) as pure function
        r: length t > u

      stdout.println(`Length check: ${checker("Hello", 3)}`)

      //BiRoutine: impure, can log during transformation
      logCombiner <- () is BiRoutine of (String, Integer, String) as function
        stdout <- Stdout()
        stdout.println(`Combining: ${t} with ${u}`)
        r: `${t}-${u}`

      result <- mergeWithEffects("Bob", 99, logCombiner)
      stdout.println(`BiRoutine result: ${result}`)

Common mistakes

E05150 — BiFunction is pure — dynamic functions extending BiFunction must use 'as pure function', not just 'as function'. The compiler requires the purity of the implementation to match its pure super type. See ek9 -h E05150 for details.

Incorrect:

() is BiFunction of (String, Integer, String) as function

Correct:

() is BiFunction of (String, Integer, String) as pure function
Other ways to ask this
  • What is the difference between BiFunction and BiRoutine?
  • How do I create a BiFunction of (String, Integer, Boolean)?
  • When should I use BiRoutine instead of BiFunction?

Coming from another language?

Java: java.util.function.BiFunction<T,U,R> — no purity distinction, supports andThen composition. Kotlin: (T, U) -> R — no pure/impure variants. Rust: Fn(T, U) -> R (pure) vs FnMut(T, U) -> R (stateful). Go: func(T, U) R — no purity. C#: Func<T1, T2, TResult> — no purity concept. EK9: BiFunction (pure, deterministic) vs BiRoutine (impure, can access state) — compile-time enforced.

Keywords: three-parameter, pure, function-type, biroutine, combine, side-effect, bifunction, transform, generic, two-input, impure