Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I combine fluent interfaces with a functional style in Scala?

I've been reading about the OO 'fluent interface' approach in Java, JavaScript and Scala and I like the look of it, but have been struggling to see how to reconcile it with a more type-based/functional approach in Scala.

To give a very specific example of what I mean: I've written an API client which can be invoked like this:

val response = MyTargetApi.get("orders", 24)

The return value from get() is a Tuple3 type called RestfulResponse, as defined in my package object:

// 1. Return code
// 2. Response headers
// 2. Response body (Option)
type RestfulResponse = (Int, List[String], Option[String])

This works fine - and I don't really want to sacrifice the functional simplicity of a tuple return value - but I would like to extend the library with various 'fluent' method calls, perhaps something like this:

val response = MyTargetApi.get("customers", 55).throwIfError()
// Or perhaps:
MyTargetApi.get("orders", 24).debugPrint(verbose=true)

How can I combine the functional simplicity of get() returning a typed tuple (or similar) with the ability to add more 'fluent' capabilities to my API?

like image 570
Alex Dean Avatar asked Dec 30 '11 16:12

Alex Dean


People also ask

Why use fluent interface?

The Fluent Interface pattern is useful when you want to provide an easy readable, flowing API. Those interfaces tend to mimic domain specific languages, so they can nearly be read as human languages. Method chaining - calling a method returns some object on which further methods can be called.

What is fluent design pattern?

Fluent is an open-source, cross-platform design system that gives designers and developers the frameworks they need to create engaging product experiences—accessibility, internationalization, and performance included.

What is fluent interface in C#?

A fluent interface is an object-oriented API that depends largely on method chaining. The goal of a fluent interface is to reduce code complexity, make the code readable, and create a domain specific language (DSL). It is a type of method chaining in which the context is maintained using a chain.

What is a fluent interface Python?

In software engineering, a fluent interface is an object-oriented API whose design relies extensively on method chaining. Its goal is to increase code legibility by creating a domain-specific language (DSL). The term was coined in 2005 by Eric Evans and Martin Fowler.


2 Answers

It seems you are dealing with a client side API of a rest style communication. Your get method seems to be what triggers the actual request/response cycle. It looks like you'd have to deal with this:

  • properties of the transport (like credentials, debug level, error handling)
  • providing data for the input (your id and type of record (order or customer)
  • doing something with the results

I think for the properties of the transport, you can put some of it into the constructor of the MyTargetApi object, but you can also create a query object that will store those for a single query and can be set in a fluent way using a query() method:

MyTargetApi.query().debugPrint(verbose=true).throwIfError()

This would return some stateful Query object that stores the value for log level, error handling. For providing the data for the input, you can also use the query object to set those values but instead of returning your response return a QueryResult:

class Query {
  def debugPrint(verbose: Boolean): this.type = { _verbose = verbose; this }
  def throwIfError(): this.type = { ... }
  def get(tpe: String, id: Int): QueryResult[RestfulResponse] =
    new QueryResult[RestfulResponse] {
       def run(): RestfulResponse = // code to make rest call goes here
    }
}

trait QueryResult[A] { self =>
  def map[B](f: (A) => B): QueryResult[B] = new QueryResult[B] {
    def run(): B = f(self.run())
  }
  def flatMap[B](f: (A) => QueryResult[B]) = new QueryResult[B] {
    def run(): B = f(self.run()).run()
  }
  def run(): A
}

Then to eventually get the results you call run. So at the end of the day you can call it like this:

MyTargetApi.query()
  .debugPrint(verbose=true)
  .throwIfError()
  .get("customers", 22)
  .map(resp => resp._3.map(_.length)) // body
  .run()

Which should be a verbose request that will error out on issue, retrieve the customers with id 22, keep the body and get its length as an Option[Int].

The idea is that you can use map to define computations on a result you do not yet have. If we add flatMap to it, then you could also combine two computations from two different queries.

like image 85
huynhjl Avatar answered Oct 16 '22 06:10

huynhjl


To be honest, I think it sounds like you need to feel your way around a little more because the example is not obviously functional, nor particularly fluent. It seems you might be mixing up fluency with not-idempotent in the sense that your debugPrint method is presumably performing I/O and the throwIfError is throwing exceptions. Is that what you mean?

If you are referring to whether a stateful builder is functional, the answer is "not in the purest sense". However, note that a builder does not have to be stateful.

case class Person(name: String, age: Int)

Firstly; this can be created using named parameters:

Person(name="Oxbow", age=36)

Or, a stateless builder:

object Person {
  def withName(name: String) 
    = new { def andAge(age: Int) = new Person(name, age) } 
}

Hey presto:

scala> Person withName "Oxbow" andAge 36

As to your use of untyped strings to define the query you are making; this is poor form in a statically-typed language. What is more, there is no need:

sealed trait Query
case object orders extends Query

def get(query: Query): Result

Hey presto:

api get orders

Although, I think this is a bad idea - you shouldn't have a single method which can give you back notionally completely different types of results


To conclude: I personally think there is no reason whatsoever that fluency and functional cannot mix, since functional just indicates the lack of mutable state and the strong preference for idempotent functions to perform your logic in.

Here's one for you:

args.map(_.toInt)

args map toInt

I would argue that the second is more fluent. It's possible if you define:

val toInt = (_ : String).toInt

That is; if you define a function. I find functions and fluency mix very well in Scala.

like image 26
oxbow_lakes Avatar answered Oct 16 '22 07:10

oxbow_lakes