How do I use UnaryOperator for same-type transformations?

← Generics · Ref: Q715

UnaryOperator of T is a pure function type that takes 't' of type T and returns 'r' of the SAME type T.

UNARYOPERATOR SIGNATURE

UnaryOperator of T takes one parameter and returns the same type:

  -> t as T
  <- r as T?

The input type T and return type T are always identical.

ALWAYS PURE

UnaryOperator is always pure — there is no impure variant.

VS FUNCTION

Function of (T, R) allows different input and output types. UnaryOperator constrains both to T:

  Function of (String, Integer): String -> Integer (different types)
  UnaryOperator of String: String -> String (same type)

USING AS PARAMETER

Pass UnaryOperator as a parameter to constrain same-type transforms:

  applyOp() as pure
    -> item as String, op as UnaryOperator of String
    <- result as String: op(item)

USE CASES

- String transformations (trim, case conversion)
- Numeric adjustments (doubling, negation)
- Same-type transforms in stream map operations

See Q54 for pure function concepts. See Q710 for Function/Routine (different types). See Q89 for stream map operations.

Example

defines module qa.genericsdeep.unaryoperator

  defines function

    <?-
      A user-defined abstract function with same-type signature.
      Mirrors UnaryOperator's pattern but user-defined for demonstration.
    -?>
    stringTransform() as pure abstract
      -> item as String
      <- result as String?

    <?-
      Named implementation: converts to uppercase.
    -?>
    toUpper() is stringTransform as pure
      -> item as String
      <- result as String: item.upperCase()

    <?-
      Named implementation: trims whitespace.
    -?>
    toTrimmed() is stringTransform as pure
      -> item as String
      <- result as String: item.trim()

    <?-
      Helper that accepts a UnaryOperator as parameter.
      Pure context — UnaryOperator is always pure.
    -?>
    applyOp() as pure
      ->
        item as String
        op as UnaryOperator of String
      <- result as String: op(item)

    <?-
      Helper that accepts the user-defined stringTransform.
    -?>
    applyTransform() as pure
      ->
        item as String
        op as stringTransform
      <- result as String: op(item)

  defines program

    UnaryOperatorDemo()
      stdout <- Stdout()

      //User-defined functions mirroring UnaryOperator pattern
      stdout.println(`Upper: ${applyTransform("hello world", toUpper)}`)
      stdout.println(`Trimmed: '${applyTransform("  spaced  ", toTrimmed)}'`)

      //Chained application
      result <- applyTransform(applyTransform("  hello  ", toTrimmed), toUpper)
      stdout.println(`Trimmed and uppercased: ${result}`)

Common mistakes

E05150 — When extending a pure abstract function (like a user-defined UnaryOperator pattern), the implementation must be marked 'as pure'. The compiler requires purity to match the super function. See ek9 -h E05150 for details.

Incorrect:

toUpper() is stringTransform
      -> item as String
      <- result as String: item.upperCase()

Correct:

toUpper() is stringTransform as pure
      -> item as String
      <- result as String: item.upperCase()
Other ways to ask this
  • What is UnaryOperator of T and how does it differ from Function?
  • How do I create a UnaryOperator of String?
  • When should I use UnaryOperator instead of Function?

Coming from another language?

Java: java.util.function.UnaryOperator<T> extends Function<T,T> — no purity distinction, can have side effects. Kotlin: (T) -> T — no named type, no purity. Rust: Fn(T) -> T — structural, no named type. Go: func(T) T — no purity. C#: Func<T, T> — no named type, no purity. EK9: UnaryOperator is always pure, constrains input=output type, separate from Function — compile-time enforced.

Keywords: map, pure, unaryoperator, same-type, function-type, string, stream, transform, generic, identity