Match Functions

Stateful functions provide a powerful abstraction for working with events and state, allowing developers to build components that can react to any kind of message. Commonly, functions only need to handle a known set of message types, and the StatefulMatchFunction interface provides an opinionated solution to that problem.

Common Patterns

Imagine a greeter function that wants to print specialized greeters depending on the type of input.

package com.ververica.statefun.docs.match;

import com.ververica.statefun.sdk.Context;
import com.ververica.statefun.sdk.StatefulFunction;

public class FnUserGreeter implements StatefulFunction {

  @Override
  public void invoke(Context context, Object input) {
    if (input instanceof Employee) {
      Employee message = (Employee) input;

      if (message.isManager()) {
        greetManager(context, message);
      } else {
        greetEmployee(context, message);
      }
    } else if (input instanceof Customer) {
      Customer message = (Customer) input;
      greetCustomer(context, message);
    } else {
      throw new IllegalStateException("Unknown message type " + input.getClass());
    }
  }

  private void greetManager(Context context, Employee message) {
    System.out.println("Hello manager " + message.getEmployeeId());
  }

  private void greetEmployee(Context context, Employee message) {
    System.out.println("Hello employee " + message.getEmployeeId());
  }

  private void greetCustomer(Context context, Customer message) {
    System.out.println("Hello customer " + message.getName());
  }
}

Customers receive one standard message, and employees receive a personalized message depending on whether or not they are managers. The input is expected to be from a set of known classes. Certain variants perform some type specific checks and then call the appropriate action.

Simple Match Function

Stateful match functions are an opinionated variant of stateful functions for precisely this pattern. Developers outline expected types, optional predicates, and well-typed business logic and let the system dispatch each input to the correct action. Variants are bound inside a configure method that is executed once the first time an instance is loaded.

package com.ververica.statefun.docs.match;

import com.ververica.statefun.sdk.Context;
import com.ververica.statefun.sdk.match.MatchBinder;
import com.ververica.statefun.sdk.match.StatefulMatchFunction;

public class FnMatchGreeter extends StatefulMatchFunction {

  @Override
  public void configure(MatchBinder binder) {
    binder
        .predicate(Customer.class, this::greetCustomer)
        .predicate(Employee.class, Employee::isManager, this::greetManager)
        .predicate(Employee.class, this::greetEmployee);
  }

  private void greetManager(Context context, Employee message) {
    System.out.println("Hello manager " + message.getEmployeeId());
  }

  private void greetEmployee(Context context, Employee message) {
    System.out.println("Hello employee " + message.getEmployeeId());
  }

  private void greetCustomer(Context context, Customer message) {
    System.out.println("Hello customer " + message.getName());
  }
}

Making Your Function Complete

Similar to the first example, match functions are partial by default and will throw an IllegalStateException on any input that does not match any branch. They can be made complete by providing an otherwise clause that serves as a catch-all for unmatched input, think of it as a default clause in a Java switch statement. The otherwise action takes its message as an untyped java.lang.Object, allowing you to handle any unexpected messages.

package com.ververica.statefun.docs.match;

import com.ververica.statefun.sdk.Context;
import com.ververica.statefun.sdk.match.MatchBinder;
import com.ververica.statefun.sdk.match.StatefulMatchFunction;

public class FnMatchGreeterWithCatchAll extends StatefulMatchFunction {

  @Override
  public void configure(MatchBinder binder) {
    binder
        .predicate(Customer.class, this::greetCustomer)
        .predicate(Employee.class, Employee::isManager, this::greetManager)
        .predicate(Employee.class, this::greetEmployee)
        .otherwise(this::catchAll);
  }

  private void catchAll(Context context, Object message) {
    System.out.println("Hello unexpected message");
  }

  private void greetManager(Context context, Employee message) {
    System.out.println("Hello manager");
  }

  private void greetEmployee(Context context, Employee message) {
    System.out.println("Hello employee");
  }

  private void greetCustomer(Context context, Customer message) {
    System.out.println("Hello customer");
  }
}

Action Resolution Order

Match functions will always match actions from most to least specific using the following resolution rules.

First, find an action that matches the type and predicate. If two predicates will return true for a particular input, the one registered in the binder first wins. Next, search for an action that matches the type but does not have an associated predicate. Finally, if a catch-all exists, it will be executed or an IllegalStateException will be thrown.