Compositional Programming with Multi-Paradigm Languages

Abstract

Compositional programming, which promotes parametric polymorphism with approaches such as Composition over Inheritance is practiced within many paradigms. It facilitates code reuse, encapsulation and open/closed principles among others. Its applicability in Multi-Paradigm languages, as defended by Martin Odersky et al, particularly at higher levels with complex structures as in Object-Oriented or Structured approaches, and lower levels with function composition, facilitates intuitive decomposition and implementation of largely scalable complex systems. We demonstrate how we make use of composition methods by discussing a project we built with the Object-Functional approach, showing how we employ function composition to build lower-level constructs; and how we go up to higher levels of integration through interacting objects to complete our architecture.


Lost in Paradigms

We are witnessing the advent of Functional Programming (FP) along with multi-paradigm languages like D, Scala and Swift. In an age where Cloud computing and BigData are the hype, it is no wonder that with its own solutions to concurrent and distributed programming FP is fast becoming popular. But why do we not switch to (almost) purely functional languages?

The de facto programming paradigm of the last decade was Object Oriented Programming (OOP), which superseded mainstream Procedural and Structured approaches. It introduced objects that bring together state and behavior; allowing SOLID principles through encapsulation, inheritance and polymorphism.

Java, the most common OOP language led the paradigm shift. However, it did not necessarily forfeit all the relics of its ancestors. In fact, the very existence of static scope is enough to deem Java not purely object oriented. But if OOP was the new age, why did we allow certain aspects of supposedly archaic paradigms to linger on?

Perhaps “video did not kill the radio star,” but simply provided an arguably more stimulating alternative. Many programming paradigms, new and old, yet coexist. Neither of them can satisfy the seemingly impossible task of modeling real world problems, or mirroring our thought patterns on a digital computer. However, each paradigm comes with certain tools and concepts that makes programming somehow more intuitive.

Object-Functional Composition in Practice

The big question is choosing the right paradigm to approach a given software problem; to which, unfortunately there is no categorical answer. However; given that we are not fanatic followers of dogmatic ideas, the best approach might actually be to use multiple paradigms within a single project, exploiting their strengths while avoiding their weaknesses.

One problem we tackled was creating a generic backend system, which provided an abstraction over Business Process along with certain rudimentary components. With scalability as one of the primary concerns, we took the functional road and made sure that we would depend on as little state as possible. We defined certain atomic units of the business function model to serve as building blocks, and went on to build systems in terms of these units.

Modularization requires bringing together certain behavior and state, and none does it better than OOP which is designed particularly for this concern. Therefore, while we defined our generic constructs as functions, we brought them together within modules defined as objects. Scala language, my personal favorite after more than a decade with Java, which allows and encourages such multi-paradigm approach, was our language of choice.


Function Composition

The rudimentary object is a Service, whose purpose is to respond to requests of type R. It provides a Context of type T, on which to process requests.

trait Service {

  type T
  type R[-X, +Y] <: Req[X, Y]

  protected def context: Context[T]

The abstract behavior are parse, interpret and validate methods which are required to unmarshal a request, transform it into a Task and make sure it is eligible for processing.

  def parse: PartialFunction[Any, R[T, Any]]

  protected def interpret[B]: Inpt[T, B, R[T, B]]
  protected def validate: Vald = allowAll

Then it runs the Operator within this task on the context. Note that this is low-level composition, meaning that architectural concerns are not addressed, nor restricted within this scope.

  protected def process[B] = new Exec[T, B] {
    override def execute(in: Task[T, B])(implicit exCon: ExCon) =      
      in.op.execute(context)
  }

  def execute[B](implicit exCon: ExCon) =
    (interpret[B] andThen (validate guards process[B]).execute) orElse unknownOperation

In order to facilitate function composition, we typed our functions within a hierarchy. We introduced the type Step, an abstraction of functions which take single input and return Future (abbreviated as Fut) values of results (Res). Its base implementation is class Phase, with abstract execute method.

type Step[-A, +B] = Phase[A, B]

trait Phase[-A, +B] {
  def execute(in: A)(implicit exCon: ExCon): Fut[Res[B]]
}

We also provided convenience functions for binding these steps through functional terms to implement complex flows, as well as implicit wrappers to make defining behavior more intuitive.

implicit class PhaseWrapper[A, B](val value: Step[A, B]) extends AnyVal {
    def merge[C](next: Step[B, C], handler: Hand[C]): Step[A, C] = 
      Phase.merge(value, next, handler)
  }

  implicit class ValidationWrapper[A, B](val value: Vald) extends AnyVal {

    def and(next: Vald): Vald = Phase.merge(value, next)
    def guards(next: Exec[A,  B], handler: Hand[B] =
      Phase.propagate): Exec[A, B] = Phase.guard(value, next, handler)

  }

Given that an implementing class does not hold mutable state, an its behavior implemented with referential transparency; it inherently allows concurrent access we end up creating complex behavior in functional terms while also adhering to FP constraints. However, the very fact that the implementing Object is a Class means that we already set foot on planet Object-Orientation.

Implementation of Task, Phase, Service and convenience methods are the following.

case class Task[A, +B](domain: Domn, user: Opt[User], op: Oper[A, B])
type Inpt[A, B, R <: Req[A, B]] = PartialFunction[R, Task[A, B]]
type Proc[A, B, R <: Req[A, B]] = PartialFunction[R, Fut[Res[B]]]

trait Service {
  import Implicits._
  type T
  type R[-X, +Y] <: Req[X, Y]

  def parse: PartialFunction[Any, R[T, Any]]

  protected def context: Context[T]
  protected def interpret[B]: Inpt[T, B, R[T, B]]
  protected def validate: Vald = allowAll

  protected def process[B] = new Exec[T, B] {
    override def execute(in: Task[T, B])(implicit exCon: ExCon) =      
      in.op.execute(context)
  }

  def execute[B](implicit exCon: ExCon) =
    (interpret[B] andThen (validate guards process[B]).execute)
      orElse unknownOperation
}

type Step[-A, +B] = Phase[A, B]
type Cont[+A] = Context[A]
type Oper[-A, +B] = Step[Cont[A], B]

type Exec[A, B] = Step[Task[A, B], B]
type Hand[+A] = Step[Fail, A]
type Vald = Step[AnyTask, Boolean]

trait Phase[-A, +B] {
  def execute(in: A)(implicit exCon: ExCon): Fut[Res[B]]
}
object Phase {

  val propagate: Hand[Nothing] = new Hand[Nothing] {
    def execute(f: Fail)(implicit exCon: ExCon) = Fut.successful(f)
  }

  def merge(head: Vald, next: Vald): Vald = {
    new Vald {
      def execute(task: Task[_, Any])(implicit ec: ExCon) = {
        head.execute(task) flatMap {
          case Done(true, _) => next.execute(task)
          case Done(false, _) => invalid
          case f: Fail => Fut.successful(f)
        }
      }
    }
  }

  def merge[A, B, C](head: Step[A, B], next: Step[B, C],
                     handler: Hand[C] = propagate) = new Phase[A, C] {
    def execute(context: A)(implicit exCon: ExCon) = {
      head.execute(context).flatMap {
        case Done(value, _) => next.execute(value)
        case f: Fail => h.execute(f)
      }
    }
  }

  def guard[A, B](head: Vald, next: Exec[A, B],
                  handler: Hand[B] = propagate): Exec[A, B] = {
    new Exec[A, B] {
      def execute(task: Task[A, B])(implicit ec: ExCon): Fut[Res[B]] = {
        head.execute(task) flatMap {
          case Done(true, _) => next.execute(task)
          case Done(false, _) => invalid
          case f: Fail => Fut.successful(f)
        }
      }
    }
  }
}

object Implicits {

  implicit class PhaseWrapper[A, B](val value: Step[A, B]) extends AnyVal {
    def merge[C](next: Step[B, C], handler: Hand[C]): Step[A, C] = 
      Phase.merge(value, next, handler)
  }

  implicit class ValidationWrapper[A, B](val value: Vald) extends AnyVal {

    def and(next: Vald): Vald = Phase.merge(value, next)
    def guards(next: Exec[A,  B], handler: Hand[B] =
      Phase.propagate): Exec[A, B] = Phase.guard(value, next, handler)

  }
}

Object Composition

While, all behavior is constructed as in terms of function composition; the fact that Service is a trait, closest analogue to a Java interface in Scala, requires implementing missing functionality within a class, named or anonymous. Therefore, this is exactly where we begin to continue building our system, albeit this time with OOP concepts.

In order to facilitate concurrent processing, we built upon the Actor-model on Akka Framework. For the uninitiated into this yet uncommon approach; the Actor-model introduces actors as atomic logic units, which communicate only via asynchronous messages and act only upon receiving a message by taking some action, creating another actor, sending some message or changing behavior so as to respond differently to consecutive messages.

We created the Server actor trait, extending some concrete class BaseActor; which wrapped a Service to resolve incoming requests with. It is possible to wrap a single service within multiple actors, or have custom actors wrapping multiple services. The former allows real-time concurrent computation on thread-safe contexts; whereas the latter allows services sharing some mutable context to be accessed in sequence.

trait Server extends BaseActor { def service: Service }
object Server {

  class Contained(val service: Service) extends Server {

    override def receive: Receive = propagate orElse unknown

    def propagate: Receive = service.parse andThen { res =>
      implicit val ec = context.dispatcher
      val target = sender()
      service.execute(ec)(res) pipeTo target
    }

  }
  object Contained

}

Modularization

Once we have our rudimentary services, we continue abstraction by generalizing each service cluster as a Component, bringing them together within an ActorSystem. A component is initialized by providing an actor system to run on, and an optional reference to an actor that might potentially be depended on by the component. The output is some actor system, and optionally an actor that other units may refer to for depending on this component.

The architecture is layered, meaning any given component depends only on some component on the layer immediately below its own. We identified the layers as Façade for system interface, Logic for business logic and Repo for persistence. Each component is supervised and represented by an actor, known as Receptionist within the pattern of the same name.

In a scenario where each layer is represented by a single component, we initialize the system with a Standalone component, which takes a component for each layer as input and sequentially initialized them from the bottom layer up. By itself a reflection of our compositional approach, it composes multiple units in Structured approach, whereas it defines its functionality through functionally composing behavior defined by aggregated units.

trait Component extends data.Named {

  def name: String
  def init: Component.Init
  def role: Component.Role
}

object Component {

  type Parm = (akka.actor.ActorSystem, Opt[akka.actor.ActorRef])
  type Init = Parm => Parm

  sealed trait Role extends data.Named

  object Role {

    abstract class Abstract(val name: String) extends Role
    case object Repository extends Abstract("repository")
    case object Facade extends Abstract("facade")
    case object Logic extends Abstract("logic")
    case object Standalone extends Abstract("standalone")
  }

  sealed abstract class Concrete(val role: Role) extends Component
  object Concrete {

    case class Facade(name: String, init: Init) extends Concrete(Role.Facade)
    object Facade

    case class Logic(name: String, init: Init) extends Concrete(Role.Logic)
    object Logic

    case class Repo(name: String, init: Init) extends Concrete(Role.Repo)
    object Repo

    case class Standalone(name: String, facade: Facade,
                          logic: Logic, repo: Repo)
      extends Concrete(Role.Standalone) {
      override def init: Init =
        repo.init andThen logic.init andThen facade.init
    }

    object Standalone
  }
}

Conclusion

Multi-paradigm languages allow us to exploit the strengths of multiple paradigms while giving us the opportunity to make up for their shortcomings. Applying intuitive decomposition capabilities of the Object-Model, and computational advantages of developing with respect to the FunctionModel is an appealing approach in designing and implementing large and complex systems.

Scala and the Akka framework allow, facilitate and encourage Compositional programming in a multi-paradigm environment. Java and Microsoft communities also seem to embrace this approach, as observed with the inclusion of Functional Programming concepts within their otherwise Object-Oriented ecosystem. Perhaps the marriage of paradigms will prove so productive, that it will become a future de facto standard with a name of its own. Time will tell.


Sample project can be found at: https://github.com/vngrs/sample-compositional.