PL Perspectives

Perspectives on computing and technology from and for those with an interest in programming languages.

An application programming interface (API) makes it possible to access an application from within code. In this context, an application can be a single class, a software library or module, an external tool or service (like MongoDB), or even a physical artifact (like the Google Assistant). The users of an API are other programmers, who enjoy simple and intuitive interfaces that support error management, for when things are going bad.

So, how can we write better APIs? Besides, of course, proper documentation, the usability of an API is greatly influenced by its design. In this post we discuss fluent APIs, which are produced by a funky and popular design technique. We learn what fluent API is, how it benefits the programmer, and how to compose expressive fluent APIs. We also show the applicability of fluent APIs to domain-specific languages.

Fluent APIs are usually associated with object-oriented programming languages. Although we mostly present Java code excerpts, equivalent fluent APIs can be composed in any OO programming language, like C#, Go, Scala, and others.

Introduction to Fluent API

Consider the Java class StringBuilder. StringBuilder, which implements the builder design pattern and is used to efficiently build complex strings:

StringBuilder builder = new StringBuilder();
builder.append("foo");
builder.append("bar");
builder.replace(4, 6, "az");
builder.reverse();
String result = builder.toString(); // "zaboof"

The class StringBuilder has a fluent API, which means its methods can instead be invoked consecutively, like so:

String result = new StringBuilder()
  .append("foo")
  .append("bar")
  .replace(4, 6, "az")
  .reverse()
  .toString(); // "zaboof"

This sequence of fluent API invocations is called a fluent chain of method calls, or simply a chain. To make StringBuilder fluent, its methods are set to return the current object:

public StringBuilder append(String str) {
  ...
  return this; // yields the current object
}

Although method append() does not produce anything, it does return the this object, for the sole purpose of allowing fluent calls. As method append() returns a StringBuilder, its invocation can be immediately followed by another StringBuilder method call, and so on. Finally, method toString() concludes the chain by returning a string instance, achieving the goal of the StringBuilder.

A fluent API contributes to code readability by allowing multiple statements to be abbreviated to a single expression. For instance, by replacing the imperative StringBuilder method calls above with a fluent chain, we made the code much shorter, and we removed the need for  the StringBuilder variable.

Broadly speaking, to make a fluent API for class Fluent, let its methods return the type Fluent; methods that return other types (like toString()) are meant to seal the fluent chains, and optionally yield their computation results. But what objects do the fluent methods actually return? Like with StringBuilder, and builders in general, having the fluent methods return the current object is the simplest way to go.

Fluent API recipe I:

class Fluent {
  Fluent fluentMethod() { ... return this; }
  Result getResult() { ... }
}

In fluent APIs that follow this recipe, the imperative and fluent call styles are interchangeable. This may not be the case, however, if the fluent methods return new API instances: return new Fluent(); instead of return this;. API instantiation can be used to make the fluent chains immutable, or it can simply be required by the implementation. To avoid confusion, the correct way to use a fluent API is usually explained in its documentation.

Using Fluent APIs to Enforce a Protocol

While fluent APIs can be implemented in dynamic programming languages (like Python and JavaScript), they are especially potent in static languages (like Java and C++). One reason may be that the syntax of static languages tends to be more strict and cumbersome, so fluent chains spare the programmer a bit more hassle. Yet, the truly fascinating feature of fluent APIs in static languages is their ability to enforce API protocols at compile-time.

An API protocol (or, contract) is a set of rules that define correct API usage. To demonstrate, let us design a fluent API for building emails:

Mail mail = new MailBuilder()
.from("donald.t@mail.com")
.to("joe.b@mail.com")
.subject("Congratulations on recent promotion")
.body("Yours truly, Donald")
.build();

The MailBuilder protocol establishes the guidelines for composing a legal mail:
A mail includes a “sender” field, one or more “receivers”, optional “subject” and a text “body”, in this exact order.
In fluent APIs like StringBuilder discussed above, any combination of method calls can compile, even if it breaks the protocol. To avoid run-time errors, we have to state the protocol details in the class documentation (and the user has to read and adhere to them).

Nevertheless, fluent APIs in static programming languages can detect API abuses as early as compilation time. For example, our mail API forbids a call to from() to be followed by a subject() call, as it skips the senders field (to()). While designing the API, we can ensure that the return type of from() does not contain the method subject(); that way, a chain containing from().subject(), which breaks the protocol, cannot even compile, as the subject() call cannot be resolved statically.

To enforce a protocol with a fluent API, we first formalize it in a (deterministic) finite state machine (FSM) diagram. The FSM below precisely captures the mail API protocol.

`MailBuilder` protocol as finite state machine

The diagram consists of nodes, which illustrate possible API states, and labeled, directed edges between them, describing API methods that apply state transitions. For instance, the mail API starts in Empty mail state, but when method from() is called, then the API state changes to Sender set, indicating that the sender field has been set. We also see that the subject field is optional, as one can transition directly from state Receiver set to state Body set by calling body(). A complete API invocation ends in accepting states, marked with double-line border; in our case, we allow build() to be called only after the body has been set.

After representing the protocol with an FSM, we can directly implement it in a fluent API. We turn every state into a class, and each edge into a method; given an edge labeled f from state X to state Y, we add method f() to class X and give the method the return type Y. To every accepting state, we also add terminating methods (build()).

Fluent API recipe II:
Describe the API protocol with a finite state machine.

class State {
  State loop() { ... }
  OtherState transition() { ... }
}
class OtherState {
  YetAnotherState otherTransition() { ... }
  Result getResult() { ... }
}

For example, the Receiver set state is encoded as follows:

class ReceiverSet {
  ReceiverSet to(String receiver) { ... }
  SubjectSet subject(String subject) { ... }
  BodySet body(String content) { ... }
}

The resulting fluent API essentially simulates the FSM in the host type system, so only fluent chains that respect the protocol can compile:

Mail mail = new EmptyMail() // returns EmptyMail
  .from("joe.b@mail.com") // returns SenderSet
  .to("donald.t@mail.com") // returns ReceiverSet
  .subject("Re: Congratulations on recent promotion") // returns SubjectSet
  .body("Thank you.") // returns BodySet
  .build(); // returns Mail

If, for instance, the call to subject() in the chain above is omitted, resulting in illegal mail, then the code would not compile, as the call to body() from ReceiverSet could not be resolved.

Besides the brute enforcement of a protocol, a fluent API also integrates neatly with certain IDE services. Auto-completion is a common IDE feature that suggests possible code continuations. When applied to a fluent API, auto-completion actively guides the programmer through the API, by showing what calls may continue the chain, while ignoring methods that would break it.

`MailBuilder` fluent API auto-completion

Unfortunately, the fluent API implementation may end up messy and unmaintainable, because it is composed of multiple classes. If two FSM edges have the same label, then we have to encode them as identical methods in two different API classes. Also, as fluent methods may not return the class they are contained in, they have to instantiate a new object, and pass it any intermediate data collected in the chain. Both of these problems can be solved by encoding fluent API states as interfaces, rather than classes. Then, we create a single master class that implements all interfaces, so its methods can simply return the this instance. That way, we not only concentrate the API logic in a single class, but we also avoid multiple object instantiations in a single chain. This alternative design still gives the benefits of a fluent API, assuming the master class is not visible to the user.

Fluent API recipe III:
Describe the API protocol with a finite state machine.

interface InitialState {
  InitialState foo();
  OtherState bar();
}
interface OtherState {
  YetAnotherState bar();
  Result baz();
}
private class APIImpl implements InitialState, OtherState, ... {
  APIImpl foo() { ... return this; }
  APIImpl bar() { ... return this; }
  Result baz() { ... }
}
public static InitialState startChain() { return new APIImpl(); }

The final encoding of the MailBuilder protocol in Java is shown at the end of the post.

Advanced Fluent API Techniques

In contrast to general-purpose programming languages, like Java and C++, domain-specific languages (DSLs) are tailored for specific tasks or applications. SQL, for example, is a famous DSL for composing database queries:

SELECT name FROM students WHERE grade>90

To submit queries from within a program, we need to embed SQL in our programming language as an API. It is possible, of course, to write DSL programs in strings, and then parse them at run-time:

var query = SQL.parse("SELECT name FROM %s WHERE grade>90", DB.students);

In this case, though, DSL syntax errors are raised only at run time; other, more obscure pitfalls are also possible.

A fluent API is a neat solution for embedding a DSL. If the syntax of the DSL can be defined as an FSM, then we can turn it into a fluent API using the method described above and catch syntax errors at compile time. We encode DSL keywords as methods and DSL literals (like numbers and strings) and expressions as parameters to these methods:

var query = SQL.SELECT("name").FROM(DB.students)
               .WHERE(student -> student.grade>90);

Fluent APIs have also received academic attention in recent years. While the art of fluent API design is generally folklore, researchers look for new methods to encode more complex families of protocols and DSLs. As it turns out, it is possible to encode non-regular protocols (that cannot be described by FSMs) as fluent APIs, by using generic (polymorphic) types. These APIs, however, are usually monolithic, and their structure is obfuscated. A rising research trend is, therefore, to provide a tool that automatically produces a fluent API from a specification—a fluent API generator. For example, Silverchain, Fling, and TypeLevelLR are all fluent API generators that convert context-free grammars, representing API protocols or DSLs, into fluent APIs.

Conclusions

A fluent API is invoked by a chain of consecutive method calls. It is a trendy practice for making elegant and concise interfaces. In this post we learned how to implement basic fluent APIs, showed that they can enforce API contracts at compile-time, and saw that they can even be used to embed domain-specific languages.

Bio: Ori Roth is a PhD student at the Technion, researching metaprogramming applications of type theory. Ori is a co-author of Fling—A Fluent API Generator.

import java.util.*;
public class Mail {
  public String sender;
  public List receivers = new ArrayList<>();
  public String subject = "";
  public String body = "";
  private Mail() {} // Hide default constructor
  public static EmptyMail builder() { return new MailBuilder(); }
  interface EmptyMail {
    SenderSet from(String sender);
  }
  interface SenderSet {
    ReceiverSet to(String receiver);
  }
  interface ReceiverSet {
    SubjectSet subject(String subject);
    BodySet body(String body);
  }
  interface SubjectSet {
    BodySet body(String content);
  }
  interface BodySet {
    Mail build();
  }
  private static class MailBuilder implements EmptyMail,
      SenderSet, ReceiverSet, SubjectSet, BodySet {
    Mail mail = new Mail();
    @Override public MailBuilder from(String sender) {
      mail.sender = sender;
      return this;
    }
    @Override public MailBuilder to(String receiver) {
      mail.receivers.add(receiver);
      return this;
    }
    @Override public MailBuilder subject(String subject) {
      mail.subject = subject;
      return this;
    }
    @Override public MailBuilder body(String content) {
      mail.body = content;
      return this;
    }
    @Override public Mail build() {
      return mail;
    }
  }
  // Usage example
  public static void main(String[] args) {
    Mail mail = Mail.builder()
      .from("donald.t@mail.com")
      .to("joe.b@mail.com")
      .subject("Congratulations on recent promotion")
      .body("Yours truly, Donald")
      .build();
  }
}

Disclaimer: These posts are written by individual contributors to share their thoughts on the SIGPLAN blog for the benefit of the community. Any views or opinions represented in this blog are personal, belong solely to the blog author and do not represent those of ACM SIGPLAN or its parent organization, ACM.