All Projects → monadless → Monadless

monadless / Monadless

Licence: apache-2.0
Syntactic sugar for monad composition in Scala

Programming Languages

scala
5932 projects
scalajs
39 projects

Projects that are alternatives of or similar to Monadless

Cats Mtl
cats transformer type classes.
Stars: ✭ 238 (+3.03%)
Mutual labels:  monad, cats
free-arrow
Implementation of the Free Arrow in Scala and other helpful tools for working with Arrows
Stars: ✭ 14 (-93.94%)
Mutual labels:  cats, monad
Cats Stm
An STM implementation for Cats Effect
Stars: ✭ 106 (-54.11%)
Mutual labels:  monad, cats
Pulsar4s
Idiomatic, typesafe, and reactive Scala client for Apache Pulsar
Stars: ✭ 172 (-25.54%)
Mutual labels:  cats
Hammock
Purely functional HTTP client
Stars: ✭ 174 (-24.68%)
Mutual labels:  cats
Iteratee
Iteratees for cats
Stars: ✭ 186 (-19.48%)
Mutual labels:  cats
Future
High-performance Future implementation for the JVM
Stars: ✭ 223 (-3.46%)
Mutual labels:  futures
Mostly Adequate Guide Chinese
函数式编程指北中文版
Stars: ✭ 2,093 (+806.06%)
Mutual labels:  monad
Fahrenheit
toy futures executor 🚒📖🔥
Stars: ✭ 213 (-7.79%)
Mutual labels:  futures
Fluture
🦋 Fantasy Land compliant (monadic) alternative to Promises
Stars: ✭ 2,249 (+873.59%)
Mutual labels:  monad
Nine Cards V2
An Open Source Android Launcher built with Scala on Android
Stars: ✭ 181 (-21.65%)
Mutual labels:  cats
Shims
Seamless interop layer between cats and scalaz
Stars: ✭ 174 (-24.68%)
Mutual labels:  cats
Trading Server
A multi-asset, multi-strategy, event-driven trade execution and management platform for running many algorithms/bots at many venues simultaneously with unified risk management and reporting. Uses MongoDB for storage and Telegram for user notifications/trade consent.
Stars: ✭ 191 (-17.32%)
Mutual labels:  futures
Smol
A small and fast async runtime for Rust
Stars: ✭ 2,206 (+854.98%)
Mutual labels:  futures
Natchez
functional tracing for cats
Stars: ✭ 214 (-7.36%)
Mutual labels:  cats
Http4s
A minimal, idiomatic Scala interface for HTTP
Stars: ✭ 2,173 (+840.69%)
Mutual labels:  cats
Scala Exercises
The easy way to learn Scala.
Stars: ✭ 2,431 (+952.38%)
Mutual labels:  cats
Q
A platform-independent promise library for C++, implementing asynchronous continuations.
Stars: ✭ 179 (-22.51%)
Mutual labels:  futures
Functional Examples
Examples with Functional JavaScript, following Professor Frisby's course
Stars: ✭ 179 (-22.51%)
Mutual labels:  monad
Funcadelic.js
Functional programming and category theory for everyday JavaScript development
Stars: ✭ 183 (-20.78%)
Mutual labels:  monad

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

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].