Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How To Convert the Builder Pattern to a Functional Implementation?

The grpc-java library is a good example of a library that utilizes the common builder pattern for creating objects with particular properties:

val sslContext = ???

val nettyChannel : NettyChannel = 
  NettyChannelBuilder
    .forAddress(hostIp, hostPort)
    .useTransportSecurity()
    .sslContext(sslContext) 
    .build

Given a library that uses this pattern how can it be wrapt so that a proper functional API can be made available? I would imagine a monad is the appropriate tool to use.

A basic first attempt would look like:

val updateBuilder : (NettyChannelBuilder => Unit) => NettyChannelBuilder => NettyChannelBuilder = 
  updateFunc => builder => {
    updateFunc(builder)
    builder
  } 

val addTransportSecurity : NettyChannelBuilder => Unit = 
  (_ : NettyChannelBuilder).useTransportSecurity()

val addSslContext : NettyChannelBuilder => Unit = 
  builder => {
    val sslContext = ???
    builder sslContext sslContext
  }

Although this method is verbose it would at least allow for composition:

 val builderPipeline : NettyChannelBuilder => NettyChannelBuilder =
   updateBuilder(addTransportSecurity) andThen updateBuilder(addSslContext)

 val nettyChannel = 
   builderPipeline(NettyChannelBuilder.forAddress(hostIp, hostPort)).build

One constraint: no using scalaz, cats, or some other 3rd party library. Only scala language "stuff".

Note: grpc is just an example use case, not the primary point of the question...

Thank you in advance for your consideration and response.

like image 782
Ramón J Romero y Vigil Avatar asked Oct 17 '18 13:10

Ramón J Romero y Vigil


2 Answers

Basic approach

If all the methods in the interface of the builder (except maybe build itself) just mutate the builder instance and return this, then they can be abstracted as Builder => Unit functions. This is true for NettyChannelBuilder, if I'm not mistaken. What you want to do in this case is to combine a bunch of those Builder => Unit into a single Builder => Unit, which runs the original ones consecutively.

Here is a direct implementation of this idea for NettyChannelBuilder:

object Builder {
  type Input = NettyChannelBuilder
  type Output = ManagedChannel

  case class Op(run: Input => Unit) {

    def and(next: Op): Op = Op { in =>
      this.run(in)
      next.run(in)
    }

    def runOn(in: Input): Output = {
      run(in)
      in.build()
    }
  }

  // combine several ops into one
  def combine(ops: Op*): Op = Op(in => ops.foreach(_.run(in)))

  // wrap methods from the builder interface

  val addTransportSecurity: Op = Op(_.useTransportSecurity())

  def addSslContext(sslContext: SslContext): Op = Op(_.sslContext(sslContext))

}

And you can use it like this:

val builderPipeline: Builder.Op =
  Builder.addTransportSecurity and
  Builder.addSslContext(???)

builderPipeline runOn NettyChannelBuilder.forAddress("localhost", 80)

Reader Monad

It's also possible to use the Reader monad here. Reader monad allows combining two functions Context => A and A => Context => B into Context => B. Of course every function you want to combine here is just Context => Unit, where the Context is NettyChannelBuilder. But the build method is NettyChannelBuilder => ManagedChannel, and we can add it into the pipeline with this approach.

Here is an implementation without any third-party libraries:

object MonadicBuilder {
  type Context = NettyChannelBuilder

  case class Op[Result](run: Context => Result) {
    def map[Final](f: Result => Final): Op[Final] =
      Op { ctx =>
        f(run(ctx))
      }

    def flatMap[Final](f: Result => Op[Final]): Op[Final] =
      Op { ctx =>
        f(run(ctx)).run(ctx)
      }
  }

  val addTransportSecurity: Op[Unit] = Op(_.useTransportSecurity())

  def addSslContext(sslContext: SslContext): Op[Unit] = Op(_.sslContext(sslContext))

  val build: Op[ManagedChannel] = Op(_.build())
}

It's convenient to use it with the for-comprehension syntax:

val pipeline = for {
  _ <- MonadicBuilder.addTransportSecurity
  sslContext = ???
  _ <- MonadicBuilder.addSslContext(sslContext)
  result <- MonadicBuilder.build
} yield result

val channel = pipeline run NettyChannelBuilder.forAddress("localhost", 80)

This approach can be useful in more complex scenarios, when some of the methods return other variables, which should be used in later steps. But for NettyChannelBuilder where most functions are just Context => Unit, it only adds unnecessary boilerplate in my opinion.

As for other monads, the main purpose of State is to track changes to a reference to an object, and it's useful because that object is normally immutable. For a mutable object Reader works just fine.

Free monad is used in similar scenarios as well, but it adds much more boilerplate, and its usual usage scenario is when you want to build an abstract syntax tree object with some actions/commands and then execute it with different interpreters.

Generic builder

It's quite simple to adapt the previous two approaches to support any builder or mutable class in general. Though without creating separate wrappers for mutating methods, the boilerplate for using it grows quite a bit. For example, with the monadic builder approach:

class GenericBuilder[Context] {
  case class Op[Result](run: Context => Result) {
    def map[Final](f: Result => Final): Op[Final] =
      Op { ctx =>
        f(run(ctx))
      }

    def flatMap[Final](f: Result => Op[Final]): Op[Final] =
      Op { ctx =>
        f(run(ctx)).run(ctx)
      }
  }

  def apply[Result](run: Context => Result) = Op(run)

  def result: Op[Context] = Op(identity)
}

Using it:

class Person {
  var name: String = _
  var age: Int = _
  var jobExperience: Int = _

  def getYearsAsAnAdult: Int = (age - 18) max 0

  override def toString = s"Person($name, $age, $jobExperience)"
}

val build = new GenericBuilder[Person]

val builder = for {
  _ <- build(_.name = "John")
  _ <- build(_.age = 36)
  adultFor <- build(_.getYearsAsAnAdult)
  _ <- build(_.jobExperience = adultFor)
  result <- build.result
} yield result

// prints: Person(John, 36, 18) 
println(builder.run(new Person))
like image 166
Kolmar Avatar answered Nov 15 '22 22:11

Kolmar


I know that we said no cats et al. but I decided to post this up, first, in all honesty as an exercise for myself and second, since in essence these libraries simply aggregate "common" typed functional constructs and patterns.

After all, would you ever consider writing an HTTP server from vanilla Java/Scala or would you grab a battle tested one off the shelf? (sorry for the evangelism)

Regardless, you could replace their heavyweight implementation with a homegrown one of your own, if you really wanted.

I will present below, two schemes that came to mind, the first using the Reader monad, the second using the State monad. I personally find the first approach a bit more clunky than the second, but they are both not too pretty on the eye. I guess that a more experienced practitioner could do a better job at it than I.

Before that, I find the following rather interesting: Semicolons vs Monads


The code:

I defined the Java Bean:

public class Bean {

    private int x;
    private String y;

    public Bean(int x, String y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Bean{" +
                "x=" + x +
                ", y='" + y + '\'' +
                '}';
    }
}

and the builder:

public final class BeanBuilder {
    private int x;
    private String y;

    private BeanBuilder() {
    }

    public static BeanBuilder aBean() {
        return new BeanBuilder();
    }

    public BeanBuilder withX(int x) {
        this.x = x;
        return this;
    }

    public BeanBuilder withY(String y) {
        this.y = y;
        return this;
    }

    public Bean build() {
        return new Bean(x, y);
    }
}

Now for the scala code:

import cats.Id
import cats.data.{Reader, State}

object Boot extends App {

  val r: Reader[Unit, Bean] = for {
    i <- Reader({ _: Unit => BeanBuilder.aBean() })
    n <- Reader({ _: Unit => i.withX(12) })
    b <- Reader({ _: Unit => n.build() })
    _ <- Reader({ _: Unit => println(b) })
  } yield b

  private val run: Unit => Id[Bean] = r.run
  println("will come before the value of the bean")
  run()


  val state: State[BeanBuilder, Bean] = for {
    _ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withX(13)) })
    _ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withY("look at me")) })
    bean <- State[BeanBuilder, Bean]({ b: BeanBuilder => (b, b.build()) })
    _ <- State.pure(println(bean))
  } yield bean

  println("will also come before the value of the bean")
  state.runA(BeanBuilder.aBean()).value
}

The output, due to the lazy nature of the evaluation of these monads is:

will come before the value of the bean
Bean{x=12, y='null'}
will also come before the value of the bean
Bean{x=13, y='look at me'}
like image 35
Yaneeve Avatar answered Nov 15 '22 22:11

Yaneeve