How do I use Consumer and Acceptor with concrete types?

← Generics · Ref: Q707

Consumer and Acceptor are the void-returning single-parameter function types. They have identical signatures except for purity.

CONSUMER IS PURE

Consumer of T takes a single parameter 't' and returns nothing. Because it is pure, it cannot call impure functions. Consumer implementations must be side-effect free.

ACCEPTOR IS IMPURE

Acceptor of T has the same signature but is NOT pure, so it CAN have side effects like I/O.

CREATING INSTANCES

Use dynamic function syntax to create concrete instances:

  validator <- () is Consumer of String as pure function
    require t?

The body uses 't' — the parameter name defined by Consumer.
Note: Consumer is pure, so the dynamic function must use 'as pure function'.

USING AS PARAMETERS

Functions can accept Consumer or Acceptor to control what callers can do:

  processItem()
    -> item as String, handler as Consumer of String
    handler(item)

BUILT-IN INTEGRATION

Optional and Result use both:

  opt.whenPresent(myConsumer)   // Pure read-only access
  opt.whenPresent(myAcceptor)   // Can modify state

See Q54 for pure vs impure concepts. See Q87 for Result operations. See Q55 for passing functions as delegates.

Example

defines module qa.genericsdeep.consumeracceptor

  defines function

    <?-
      Helper that accepts a Consumer parameter (pure).
      Because the handler is a Consumer, this function knows
      no side effects will occur when it is called.
    -?>
    processWithConsumer() as pure
      ->
        item as String
        handler as Consumer of String
      handler(item)

    <?-
      Helper that accepts an Acceptor parameter (impure).
      The Acceptor may perform side effects like I/O.
    -?>
    processWithAcceptor()
      ->
        item as String
        handler as Acceptor of String
      handler(item)

  defines program

    ConsumerAcceptorDemo()
      stdout <- Stdout()

      //Consumer: pure, read-only — validate without side effects
      //Body uses 't' — the parameter name from Consumer's signature
      validator <- () is Consumer of String as pure function
        require t?

      processWithConsumer("Hello", validator)
      stdout.println("Consumer validation passed")

      //Acceptor: impure, can have side effects like printing
      printer <- () is Acceptor of String as function
        stdout <- Stdout()
        stdout.println(`Acceptor received: ${t}`)

      processWithAcceptor("World", printer)

      //Both work with Optional via whenPresent
      opt <- Optional("Steve")
      if opt?
        stdout.println(`Optional contains: ${opt.get()}`)

Common mistakes

E05150 — Consumer is pure — dynamic functions extending Consumer 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 Consumer of String as function

Correct:

() is Consumer of String as pure function
Other ways to ask this
  • How do I create a Consumer of String or Acceptor of Integer?
  • What is the difference between Consumer and Acceptor in practice?
  • How do I pass Consumer and Acceptor as function parameters?

Coming from another language?

Java: java.util.function.Consumer<T> — no purity distinction, all consumers can have side effects. Kotlin: (T) -> Unit — no pure/impure variants. Rust: Fn(&T) for immutable borrow, FnMut(&mut T) for mutable — structural not nominal. Go: func(T) — no purity enforcement. C#: Action<T> — no purity concept. EK9: Consumer (pure) vs Acceptor (impure) gives compile-time purity guarantees.

Keywords: acceptor, function-type, delegate, void, pure, consumer, callback, parameter, impure, generic, side-effect