Coder Social home page Coder Social logo

monadless

Syntactic sugar for monad composition (or: "async/await" generalized)

Build Status Codacy Badge Join the chat at https://gitter.im/monadless/monadless-Dependency Status Maven Central Javadocs

Problem

Dealing with monad compositions involves considerable syntax noise. For instance, this code using the Future monad:

callServiceA().flatMap { a =>
  callServiceB(a).flatMap { b =>
    callServiceC(b).map { c =>
      (a, c)
    }
  }
}

would be much easier to follow using synchronous operations, without a monad:

  val a = callServiceA()
  val b = callServiceB(a)
  val c = callServiceC(b)
  (a, c)

This issue affects the usability of any monadic interface (Future, Option, Try, etc.). As an alternative, Scala provides for-comprehensions to reduce the noise:

  for {
    a <- callServiceA()
    b <- callServiceB(a)
    c <- callServiceC(b)
  } yield {
    (a, c)
  }

They are useful to express sequential compositions and make it easy to access the results of each for-comprehension step from the following ones, but they don't provide syntax sugar for Scala constructs other than assignment (<-, =) and mapping (yield).

Solution

Most mainstream languages have support for asynchronous programming using the async/await idiom or are implementing it (e.g. F#, C#/VB, Javascript, Python, Swift). Although useful, async/await is usually tied to a particular monad that represents asynchronous computations (Task, Future, etc.).

This library implements a solution similar to async/await but generalized to any monad type. This generalization is a major factor considering that some codebases use other monads like Task in addition to Future for asynchronous computations.

Given a monad M, the generalization uses the concept of lifting regular values to a monad (T => M[T]) and unlifting values from a monad instance (M[T] => T). Example usage:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Note that lift corresponds to async and unlift to await.

Getting started

The lift and unlift methods are provided by an instance of io.monadless.Monadless. The library is generic and can be used with any monad type, but sub-modules with pre-defined Monadless instances are provided for convenience:

monadless-stdlib

SBT configuration:

// scala
libraryDependencies += "io.monadless" %% "monadless-stdlib" % "0.0.13"

// scala.js
libraryDependencies += "io.monadless" %%% "monadless-stdlib" % "0.0.13"

Imports:

// for `scala.concurrent.Future`
import io.monadless.stdlib.MonadlessFuture._

// for `scala.Option`
// note: doesn't support `try`/`catch`/`finally`
import io.monadless.stdlib.MonadlessOption._

// for `scala.util.Try`
import io.monadless.stdlib.MonadlessTry._

monadless-monix

SBT configuration:

// scala
libraryDependencies += "io.monadless" %% "monadless-monix" % "0.0.13"

// scala.js
libraryDependencies += "io.monadless" %%% "monadless-monix" % "0.0.13"

Usage:

// for `monix.eval.Task`
import io.monadless.monix.MonadlessTask._

monadless-cats

SBT configuration:

// scala
libraryDependencies += "io.monadless" %% "monadless-cats" % "0.0.13"

// scala.js
libraryDependencies += "io.monadless" %%% "monadless-cats" % "0.0.13"

Usage:

// for `cats.Applicative`
// note: doesn't support `try`/`catch`/`finally`
val myApplicativeMonadless = io.monadless.cats.MonadlessApplicative[MyApplicative]()
import myApplicativeMonadless._

// for `cats.Monad`
// note: doesn't support `try`/`catch`/`finally`
val myMonadMonadless = io.monadless.cats.MonadlessMonad[MyMonad]()
import myMonadMonadless._

monadless-algebird

SBT configuration:

libraryDependencies += "io.monadless" %% "monadless-algebird" % "0.0.13"

Usage:

// for `com.twitter.algebird.Applicative`
// note: doesn't support `try`/`catch`/`finally`
val myApplicativeMonadless = io.monadless.algebird.MonadlessApplicative[MyApplicative]()
import myApplicativeMonadless._

// for `com.twitter.algebird.Monad`
// note: doesn't support `try`/`catch`/`finally`
val myMonadMonadless = io.monadless.algebird.MonadlessMonad[MyMonad]()
import monadless._

Twitter monads

SBT configuration:

libraryDependencies += "io.monadless" %% "monadless-core" % "0.0.13"

The default method resolution uses the naming conventions adopted by Twitter, so it's possible to use the default Monadless for them:

val futureMonadless = io.monadless.Monadless[com.twitter.util.Future]()
import futureMonadless._

val tryMonadless = io.monadless.Monadless[com.twitter.util.Try]()
import tryMonadless

Other monads

SBT configuration:

// scala
libraryDependencies += "io.monadless" %% "monadless-core" % "0.0.13"

// scala.js
libraryDependencies += "io.monadless" %% "monadless-core" % "0.0.13"

See "How does it work?" for information on how to define a Monadless instance for other monads.

Supported constructs

vals:

lift {
  val i = unlift(a)
  i + 1
}

nested blocks of code:

lift {
  val i = {
     val j = unlift(a)
     j * 3
  }
  i + 1
}

val pattern matching:

lift {
  val (i, j) = (unlift(a), unlift(b))
}

if conditions:

lift {
  if(unlift(a) == 1) unlift(c)
  else 0
}

boolean operations (including short-circuiting):

lift {
  unlift(a) == 1 || (unlift(b) == 2 && unlift(c) == 3)
}

def:

lift {
  def m(j: Int) = unlift(a) + j
  m(unlift(b))
}

recursive defs:

lift {
  def m(j: Int) = if(j == 0) unlift(a) else m(j - 1)
  m(10)
}

traits, classes, and objects:

lift {
  trait A {
    def i = unlift(a)
  }
  class B extends A {
    def j = i + 1
  }
  object C {
    val k = unlift(c)
  }
  (new B).j + C.k
}

pattern matching:

lift {
  unlift(a) match {
    case 1 => unlift(b)
    case _ => unlift(c)
  }
}

try/catch/finally:

lift {
  try unlift(a)
  catch {
    case e => unlift(b)
  } finally {
    println("done")
  }
}

while loops:

lift {
  var i = 0
  while(i < 10)
    i += unlift(a)
}

The UnsupportedSpec lists the constructs that are known to be unsupported. Please report if you find a construct that can't be translated and is not classified by the spec class.

How does it work?

The unlift method is only a marker that indicates that the lift macro transformation needs to treat a value as monad instance. For example, it never blocks threads using Await.result if it's dealing with a Future.

The code generated by the macro uses an approach similar to for-comprehensions, resolving at compile time the methods that are required for the composition and not requiring a particular monad interface. We call these "ghost" methods: they aren't defined by an interface and only need to be source-compatible with the generated macro tree. To elucidate, let's take map as an example:

// Option `map` signature
def map[B](f: A => B): Option[B]

// Future `map` signature
def map[B](f: A => B)(implicit ec: ExecutionContext)

Future and Option are supported by for-comprehensions and lift even though they don't share the same method signature since Future requires an ExecutionContext. They are only required to be source-compatible with the transformed tree. Example lift transformation:

def a: Future[Int] = ???

// this transformation
lift {
  unlift(a) + 1
}

// generates the tree
a.map(_ + 1)

// that triggers scala's implicit resolution after the
// macro transformation and becomes:
a.map(_ + 1)(theExecutionContext)

For-comprehensions use only two "ghost" methods: map and flatMap. To support more Scala constructs, Monadless requires additional methods. This is the definition of the "ghost" interface that Monadless expects:

trait M[A] {
  
  // Applies the map function
  def map[B](f: A => B): M[B]

  // Applies `f` and then flattens the result
  def flatMap[B](f: A => M[B]): M[B]
  
  // Recovers from a failure if the partial function 
  // is defined for the failure. Used to translate `catch` clauses.
  def rescue(pf: PartialFunction[Throwable, M[A]]): M[A]

  // Executes `f` regarless of the outcome (success/failure).
  // Used to translate `finally` clauses.
  def ensure(f: => Unit): M[A]
}

object M {

  // Creates a monad instance with the result of `f`
  def apply[A](f: => A): M[A]

  // Transforms multiple monad instances into one.
  def collect[A](l: List[M[A]]): M[List[A]]
}

As an alternative to using the monad type methods directly since not all existing monads implement them, Monadless allows the user to define them separately:

object CustomMonadless extends Monadless[M] {

  // these are also "ghost" methods
  def apply[A](f: => A): M[A] = ???
  def collect[A](l: List[M[A]]): M[List[A]] = ???
  def map[A, B](m: M[A])(f: A => B): M[B] = ???
  def flatMap[A, B](m: M[A])(f: A => M[B]): M[B] = ???
  def rescue[A](m: M[A])(pf: PartialFunction[Throwable, M[A]]): M[A] = ??
  def ensure[A](m: M[A])(f: => Unit): M[A] = ???
}

The methods defined by the Monadless instance have precedence over the ones specified by the monad instance and its companion object

Related projects

Code of Conduct

Please note that this project is released with a Contributor Code of Conduct. By participating in this project, you agree to abide by its terms. See CODE_OF_CONDUCT.md for details.

License

See the LICENSE file for details.

Maintainers

  • @fwbrasil
  • @sameerparekh

monadless's Projects

monadless icon monadless

Syntactic sugar for monad composition in Scala

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.