Git Product home page Git Product logo

monadless's Introduction

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 People

Contributors

fwbrasil avatar johnynek avatar krever avatar sameerparekh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

monadless's Issues

catching NonFatal(_) has compile error

Thanks for this library! See details below, thanks.

Version: 0.0.13

Expected behavior

NonFatal exceptions can be caught in an async function

Actual behavior

Compiler error:

error: exception during macro expansion:
scala.reflect.internal.FatalError:
  unexpected UnApply scala.util.control.NonFatal.unapply(<unapply-selector>) <unapply> ((e @ _))
     while compiling: <console>
        during phase: typer
     library version: version 2.12.7
    compiler version: version 2.12.7
  reconstructed args: -deprecation -feature -howtorun:repl

  last tree to typer: UnApply
       tree position: line 24 of <console>
              symbol: null
           call site: method applyOrElse in package $line7

== Source file context for tree position ==

    21             unlift(bar())
    22         }
    23         catch {
    24             case NonFatal(e) => println("Exception")
    25         }
    26         "foo"
    27     }
	at scala.reflect.internal.Reporting.abort(Reporting.scala:61)
	at scala.reflect.internal.Reporting.abort$(Reporting.scala:57)
	at scala.tools.nsc.interpreter.IMain$$anon$1.scala$tools$nsc$interpreter$ReplGlobal$$super$abort(IMain.scala:240)
	at scala.tools.nsc.interpreter.ReplGlobal.abort(ReplGlobal.scala:23)
	at scala.tools.nsc.interpreter.ReplGlobal.abort$(ReplGlobal.scala:21)
	at scala.tools.nsc.interpreter.IMain$$anon$1.abort(IMain.scala:240)
	at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:5576)
	at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:5617)
	at scala.tools.nsc.typechecker.Typers$Typer.$anonfun$typedPattern$2(Typers.scala:5738)
	at scala.tools.nsc.typechecker.Typers$Typer.$anonfun$typedPattern$1(Typers.scala:5738)
	at scala.tools.nsc.typechecker.TypeDiagnostics.typingInPattern(TypeDiagnostics.scala:63)
	at scala.tools.nsc.typechecker.TypeDiagnostics.typingInPattern$(TypeDiagnostics.scala:60)
	at scala.tools.nsc.interpreter.ReplGlobal$$anon$1.typingInPattern(ReplGlobal.scala:26)
	at scala.tools.nsc.typechecker.Typers$Typer.typedPattern(Typers.scala:5738)
	at scala.tools.nsc.typechecker.Typers$Typer.typedCase(Typers.scala:2510)
	at scala.tools.nsc.typechecker.Typers$Typer.$anonfun$typedCases$1(Typers.scala:2544)
	at scala.tools.nsc.typechecker.Typers$Typer.typedCases(Typers.scala:2543)
	at scala.tools.nsc.typechecker.Typers$Typer.typedMatch(Typers.scala:2555)
	at scala.tools.nsc.typechecker.Typers$Typer.applyOrElseMethodDef$1(Typers.scala:2688)
	at scala.tools.nsc.typechecker.Typers$Typer.synthesizePartialFunction(Typers.scala:2800)
	at scala.tools.nsc.typechecker.Typers$Typer.typedVirtualizedMatch$1(Typers.scala:4458)
	at scala.tools.nsc.typechecker.Typers$Typer.typedOutsidePatternMode$1(Typers.scala:5550)
	at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:5581)
	at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:5617)
	at scala.tools.nsc.typechecker.Typers$Typer.$anonfun$typedArg$1(Typers.scala:3280)
	at scala.tools.nsc.typechecker.Typers$Typer.typedArg(Typers.scala:471)
	at scala.tools.nsc.typechecker.PatternTypers$PatternTyper.typedArgWithFormal$1(PatternTypers.scala:108)
	at scala.tools.nsc.typechecker.PatternTypers$PatternTyper.$anonfun$typedArgsForFormals$4(PatternTypers.scala:122)
	at scala.tools.nsc.typechecker.PatternTypers$PatternTyper.typedArgsForFormals(PatternTypers.scala:122)
	at scala.tools.nsc.typechecker.PatternTypers$PatternTyper.typedArgsForFormals$(PatternTypers.scala:103)
	at scala.tools.nsc.typechecker.Typers$Typer.typedArgsForFormals(Typers.scala:184)
	at scala.tools.nsc.typechecker.Typers$Typer.handleMonomorphicCall$1(Typers.scala:3619)
	at scala.tools.nsc.typechecker.Typers$Typer.doTypedApply(Typers.scala:3652)
	at scala.tools.nsc.typechecker.Typers$Typer.normalTypedApply$1(Typers.scala:4767)
	at scala.tools.nsc.typechecker.Typers$Typer.typedApply$1(Typers.scala:4776)
	at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:5571)
	at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:5617)
	at scala.tools.nsc.typechecker.Typers$Typer.typedSelectOrSuperCall$1(Typers.scala:5701)
	at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:5572)
	at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:5617)
	at scala.tools.nsc.typechecker.Typers$Typer.$anonfun$typed1$38(Typers.scala:4746)
	at scala.tools.nsc.typechecker.Typers$Typer.silent(Typers.scala:693)
	at scala.tools.nsc.typechecker.Typers$Typer.normalTypedApply$1(Typers.scala:4748)
	at scala.tools.nsc.typechecker.Typers$Typer.typedApply$1(Typers.scala:4776)
	at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:5571)
	at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:5617)
	at scala.reflect.macros.contexts.Typers.$anonfun$typecheck$4(Typers.scala:26)
	at scala.tools.nsc.typechecker.Typers$Typer.silent(Typers.scala:693)
	at scala.reflect.macros.contexts.Typers.$anonfun$typecheck$3(Typers.scala:26)
	at scala.reflect.macros.contexts.Typers.$anonfun$typecheck$2(Typers.scala:26)
	at scala.reflect.macros.contexts.Typers.doTypecheck$1(Typers.scala:25)
	at scala.reflect.macros.contexts.Typers.$anonfun$typecheck$7(Typers.scala:38)
	at scala.reflect.internal.Trees.wrappingIntoTerm(Trees.scala:1736)
	at scala.reflect.internal.Trees.wrappingIntoTerm$(Trees.scala:1733)
	at scala.reflect.internal.SymbolTable.wrappingIntoTerm(SymbolTable.scala:18)
	at scala.reflect.macros.contexts.Typers.typecheck(Typers.scala:38)
	at scala.reflect.macros.contexts.Typers.typecheck$(Typers.scala:20)
	at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
	at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
	at io.monadless.impl.Macro.lift(Macro.scala:16)

           def foo(): Future[String] = lift {

Steps to reproduce the behavior

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.control.NonFatal
import io.monadless.stdlib.MonadlessFuture._

object Test {
    def bar(): Future[String] = lift { "bar" }
    def foo(): Future[String] = lift {
        try {
            unlift(bar())
        }
        catch {
            case NonFatal(e) => println("Exception")
        }
        "foo"
    }
}

Workaround

catch Throwable

Cant create aliases for "lift/unlift"

Good day.

Aliases for "lift/unlift" don't compile =(
Example below fails with compile error

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import io.monadless.stdlib.MonadlessFuture.{lift => async, unlift => await, _}

object Test extends App {
  def a: Future[Int] = Future.successful(1)

  async {
    await(a) + 1
  }
}

error message:

Error:(10, 9) Can't typecheck the monadless transformation. Please file a bug report with this error and your `Monadless` instance. 
Failure: value await is not a member of object io.monadless.stdlib.MonadlessFuture
Tree: Future.apply(io.monadless.stdlib.MonadlessFuture.await[Int](Test.this.a).$plus(1))
  async {

Build for 2.13.0?

There's a push in the ecosystem to get ready for 2.13 ahead of time. We would like to migrate our libraries to 2.13 as well, but in that case we won't be able to use monadless ...

Bug when scala-logging

Monadless Version: 0.0.13
Scala-Logging Version: 3.9.0

There seems to be a problem when using monadless and scala-logging.

If you try to compile

import cats.effect.IO
import com.typesafe.scalalogging.StrictLogging
import io.monadless.cats.MonadlessMonad

object Foo extends StrictLogging {
  val monadlessIO: MonadlessMonad[IO] = io.monadless.cats.MonadlessMonad[IO]()
  import monadlessIO._

  lift {
    val opt = Some("foo")
    val aString = ""

    logger.error(s"$opt $aString")
  }
}

you will get this error:

[error] /tmp/orium/hunt-monadless-bug/service/src/main/scala/NewAssetProcessor.scala:9:8: Can't typecheck the monadless transformation. Please file a bug report with this error and your `Monadless` instance. 
[error] Failure: not found: value ClassTag
[error] Tree: Foo.this.monadlessIO.apply({
[error]   val opt = scala.Some.apply[String]("foo");
[error]   val aString = "";
[error]   (if (Foo.this.logger.underlying.isErrorEnabled())
[error]     Foo.this.logger.underlying.error("{} {}", (scala.Array.apply[java.io.Serializable](opt, aString)((ClassTag.apply[java.io.Serializable](classOf[java.lang.Object]): scala.reflect.ClassTag[java.io.Serializable])): _*))
[error]   else
[error]     (): Unit)
[error] })
[error]   lift {
[error]        ^

Transformation changes evalution order of code.

Thanks for the pointer to monadless. I've been trying it out and noted that:

scala> lift { (unlift(Option.empty[String]), unlift(???)) } // expected `None`

scala.NotImplementedError: an implementation is missing
  at scala.Predef$.$qmark$qmark$qmark(Predef.scala:288)
  ... 36 elided

Expected None

scala> def i(i: Int) = { println(i); Some(i) }

scala> lift { (unlift(i(1)), i(2).get, unlift(i(3))) }

1
3
2
res14: Option[(Int, Int, Int)] = Some((1,2,3))

Expected 1\n2\n\3

scala> lift { object O { def x = unlift(i(2)) }; i(1).getOrElse(O.x) }
2
1
res19: Option[Int] = Some(1)

Does not work with different nested monads

First and foremost, thanks for creating this project! I'm trying to find ways of making Scala easier for beginners to grasp and this definitely looks promising :)

Version: 0.0.13

Expected behavior

Can work with nested monads

import io.monadless.stdlib.MonadlessOption._, io.monadless.stdlib.MonadlessFuture._

val f1: Future[Option[Int]] = Future.successful(Some(1))
val f2: Future[Option[Int]] = Future.successful(Some(2))

// Doesn't work
val f3 = lift {
    val one = unlift(unlift(f1))
    val two = unlift(unlift(f2))
    one + two
  }

// Also doesn't work
val f3 = lift {
    val maybeOne = unlift(f1)
    val maybeTwo = unlift(f2)
    lift {
     unlift(maybeOne) + unlift(maybeTwo)
    }
  }

Actual behavior

Does not work]

 found   : scala.concurrent.Future[Option[Int]]
 required: io.monadless.stdlib.MonadlessOption.M[?]
    (which expands to)  Option[?]
  val maybeOne = unlift(f1)
                        ^
cmd22.sc:3: type mismatch;
 found   : scala.concurrent.Future[Option[Int]]
 required: io.monadless.stdlib.MonadlessOption.M[?]
    (which expands to)  Option[?]
  val maybeTwo = unlift(f2)

Workaround

Couldn't really find a workaround; I tried aliasing but obviously #4 means I that doesn't work.

compile failure: scala-js

Version: (e.g. 0.0.1-SNAPSHOT)
monadless: 0.13
scala-js: 1.0.0-RC2, up from 0.6.21 where it worked fine.

Expected behavior

Compile should work.

Actual behavior

Can't typecheck the monadless transformation.

Steps to reproduce the behavior

scala-js non-native JS class that uses UndefOr

class TestJS (
  val name: js.UndefOr[String] = js.undefined
) extends js.Object

code that causes the issue:

lift {
  new TestJS(name="blah")
}

Error:

importdataactions.scala:120:20: Can't typecheck the monadless transformation. Please file a bug report with this error and your `Monadless` instance. 
[error] Failure: $bar is not an enclosing class
[error] Tree: Task.apply(new TestJS(js.this.$bar.from[String, String, Unit]("blah")($bar.this.Evidence.left[String, String, Unit]($bar.this.Evidence.base[String]))))
[error]       val x = lift {
[error]                    ^

Note there is no monad in there yet, just a constant. However, in the full code I need to do this inside the lift in order to call my monad, fs2.Task.

Workaround

None identified.

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.