├── project ├── build.properties └── plugins.sbt ├── .travis.yml ├── .gitignore ├── src ├── main │ └── scala │ │ └── scala │ │ └── workflow │ │ ├── auxiliary.scala │ │ ├── workflow.scala │ │ ├── composition.scala │ │ ├── instances.scala │ │ └── package.scala └── test │ └── scala │ └── scala │ └── workflow │ ├── SemiIdiomInstancesSpec.scala │ ├── IdiomInstancesSpec.scala │ ├── FunctorInstancesSpec.scala │ ├── MonadInstancesSpec.scala │ ├── WorkflowContextSpec.scala │ ├── ReadmeSpec.scala │ └── CompositionSpec.scala └── Readme.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | script: 3 | - sbt clean test 4 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | lib 12 | 13 | # Scala-IDE specific 14 | .scala_dependencies 15 | 16 | # IntelliJ IDEA-specific 17 | .idea 18 | *.ipr 19 | *.iml 20 | *.iws 21 | -------------------------------------------------------------------------------- /src/main/scala/scala/workflow/auxiliary.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | trait Semigroup[A] { 4 | def append: (A, A) ⇒ A 5 | } 6 | 7 | object Semigroup extends SemigroupInstances { 8 | def apply[A](ap: (A, A) ⇒ A) = new Semigroup[A] { 9 | def append = ap 10 | } 11 | } 12 | 13 | trait Monoid[A] extends Semigroup[A] { 14 | val unit: A 15 | } 16 | 17 | object Monoid extends MonoidInstances { 18 | def apply[A](u: A, ap: (A, A) ⇒ A) = new Monoid[A] { 19 | val unit = u 20 | def append = ap 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/SemiIdiomInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import org.scalatest.FlatSpec 4 | import org.scalatest.matchers.ShouldMatchers 5 | 6 | class SemiIdiomInstancesSpec extends FlatSpec with ShouldMatchers { 7 | behavior of "Built-in semi-idiom instances" 8 | 9 | "ZipLists" should "work" in { 10 | context(zipList) { 11 | $(List(1, 2, 3).toString + "!") should equal (List("1!", "2!", "3!")) 12 | $(List(1, 2, 3, 4) * List(2, 3, 4)) should equal (List(2, 6, 12)) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/IdiomInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import org.scalatest.FlatSpec 4 | import org.scalatest.matchers.ShouldMatchers 5 | 6 | class IdiomInstancesSpec extends FlatSpec with ShouldMatchers { 7 | behavior of "Built-in idiom instances" 8 | 9 | "ZipStream" should "work" in { 10 | context(zipStream) { 11 | val a = Stream.from(1) 12 | $(2).take(5) should equal (Stream(2, 2, 2, 2, 2)) 13 | $(a + 1).take(5) should equal (Stream(2, 3, 4, 5, 6)) 14 | $(a * a).take(5) should equal (Stream(1, 4, 9, 16, 25)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/scala/workflow/workflow.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import language.higherKinds 4 | 5 | trait Workflow[F[_]] 6 | 7 | trait Mapping[F[_]] extends Workflow[F] { 8 | def map[A, B](f: A ⇒ B): F[A] ⇒ F[B] 9 | } 10 | 11 | trait Applying[F[_]] extends Workflow[F] with Mapping[F] { 12 | def app[A, B](f: F[A ⇒ B]): F[A] ⇒ F[B] 13 | } 14 | 15 | trait Pointing[F[_]] extends Workflow[F] { 16 | def point[A](a: ⇒ A): F[A] 17 | } 18 | 19 | trait Binding[F[_]] extends Workflow[F] { 20 | def bind[A, B](f: A ⇒ F[B]): F[A] ⇒ F[B] 21 | } 22 | 23 | trait Functor[F[_]] extends Mapping[F] with FunctorComposition[F] 24 | 25 | object Functor extends FunctorInstances 26 | 27 | trait SemiIdiom[F[_]] extends Functor[F] with Applying[F] with SemiIdiomComposition[F] 28 | 29 | object SemiIdiom extends SemiIdiomInstances 30 | 31 | trait Idiom[F[_]] extends SemiIdiom[F] with Pointing[F] with IdiomComposition[F] { 32 | def map[A, B](f: A ⇒ B) = app(point(f)) 33 | } 34 | 35 | object Idiom extends IdiomInstances 36 | 37 | trait SemiMonad[F[_]] extends SemiIdiom[F] with Binding[F] with SemiMonadComposition[F] 38 | 39 | object SemiMonad extends SemiMonadInstances 40 | 41 | trait Monad[F[_]] extends Idiom[F] with SemiMonad[F] with MonadComposition[F] { 42 | def app[A, B](f: F[A ⇒ B]) = bind(a ⇒ bind((g: A ⇒ B) ⇒ point(g(a)))(f)) 43 | } 44 | 45 | object Monad extends MonadInstances -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/FunctorInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import org.scalatest.FlatSpec 4 | import org.scalatest.matchers.ShouldMatchers 5 | 6 | class FunctorInstancesSpec extends FlatSpec with ShouldMatchers { 7 | behavior of "Built-in functor instances" 8 | 9 | "2-tuples with fixed left argument" should "work" in { 10 | context(tupleR[String]) { 11 | $(("foo", 10) * 2) should equal ("foo", 20) 12 | } 13 | } 14 | 15 | "2-tuples with fixed right argument" should "work" in { 16 | context(tupleL[Int]) { 17 | $(("foo", 10) + "bar") should equal ("foobar", 10) 18 | } 19 | } 20 | 21 | "3-tuples with fixed left argument" should "work" in { 22 | context(tuple3L[Boolean, Int]) { 23 | $(("foo", false, 10) + "bar") should equal ("foobar", false, 10) 24 | } 25 | } 26 | 27 | "3-tuples with fixed middle argument" should "work" in { 28 | context(tuple3M[String, (Int, Double)]) { 29 | $(("foo", 2, (10, 0.5)) * 4) should equal ("foo", 8, (10, 0.5)) 30 | } 31 | } 32 | 33 | "3-tuples with fixed right argument" should "work" in { 34 | context(tuple3R[String, Unit]) { 35 | $(("foo", (), 10) + 2) should equal ("foo", (), 12) 36 | } 37 | } 38 | 39 | "Maps" should "work" in { 40 | context(map[String]) { 41 | $(Map("foo" → 10, "bar" → 5, "qux" → 2) * 2) should equal (Map("foo" → 20, "bar" → 10, "qux" → 4)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/scala/workflow/composition.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import language.higherKinds 4 | 5 | trait FunctorComposition[F[_]] { f: Functor[F] ⇒ 6 | def $ [G[_]](g: Functor[G]) = new Functor[({type λ[α] = F[G[α]]})#λ] { 7 | def map[A, B](h: A ⇒ B) = f map (g map h) 8 | } 9 | def & [G[_]](g: Functor[G]) = g $ this 10 | } 11 | 12 | trait SemiIdiomComposition[F[_]] { f: SemiIdiom[F] ⇒ 13 | def $ [G[_]](g: SemiIdiom[G]) = new SemiIdiom[({type λ[α] = F[G[α]]})#λ] { 14 | def map[A, B](h: A ⇒ B) = f map (g map h) 15 | def app[A, B](h: F[G[A ⇒ B]]) = f app (f map g.app[A, B])(h) 16 | } 17 | def & [G[_]](g: SemiIdiom[G]) = g $ this 18 | } 19 | 20 | trait IdiomComposition[F[_]] { f: Idiom[F] ⇒ 21 | def $ [G[_]](g: Idiom[G]) = new Idiom[({type λ[α] = F[G[α]]})#λ] { 22 | def point[A](a: ⇒ A) = f.point(g.point(a)) 23 | def app[A, B](h: F[G[A ⇒ B]]) = f app (f map g.app[A, B])(h) 24 | } 25 | def & [G[_]](g: Idiom[G]) = g $ this 26 | } 27 | 28 | /* Monads and semi-monads in general cannot be composed. Instead, some 29 | * particular monads can provide their own specific implementation of 30 | * `$` or `&` method. Note, that even having, say, `$` method defined, monad 31 | * might not be able to define `&`. To capture this notion, `Monad[F]` can be 32 | * extended to either `LeftComposableMonad[F]` or `RightComposableMonad[F]`. 33 | * In the first case it is supposed to be able to produce `F[G[_]]` monad and 34 | * therefore implement `$` method. In the second case it is supposed to be able 35 | * to produce `G[F[_]]` monad and therefore implement `$` method. 36 | * 37 | * So, for monads `f` and `g` their composition `f $ g` will be a monad either 38 | * when `f` is left-composable or `g` is right-composable. 39 | * 40 | * The same holds for semi-monads. 41 | * 42 | * Methods `$` and `&` are called 'monad transformer' elsewhere, although 43 | * separation of left- and right- composability is usually not introduced. 44 | */ 45 | trait SemiMonadComposition[F[_]] { f: SemiMonad[F] ⇒ 46 | def $ [G[_]](g: RightComposableSemiMonad[G]): SemiMonad[({type λ[α] = F[G[α]]})#λ] = g & f 47 | def & [G[_]](g: LeftComposableSemiMonad[G]): SemiMonad[({type λ[α] = G[F[α]]})#λ] = g $ f 48 | } 49 | 50 | trait LeftComposableSemiMonad[F[_]] extends SemiMonad[F] { 51 | def $ [G[_]](g: SemiMonad[G]): SemiMonad[({type λ[α] = F[G[α]]})#λ] 52 | override def $ [G[_]](g: RightComposableSemiMonad[G]) = $(g.asInstanceOf[SemiMonad[G]]) 53 | } 54 | 55 | trait RightComposableSemiMonad[F[_]] extends SemiMonad[F] { 56 | def & [G[_]](g: SemiMonad[G]): SemiMonad[({type λ[α] = G[F[α]]})#λ] 57 | override def & [G[_]](g: LeftComposableSemiMonad[G]) = &(g.asInstanceOf[SemiMonad[G]]) 58 | } 59 | 60 | trait MonadComposition[F[_]] { f: Monad[F] ⇒ 61 | def $ [G[_]](g: RightComposableMonad[G]): Monad[({type λ[α] = F[G[α]]})#λ] = g & this 62 | def & [G[_]](g: LeftComposableMonad[G]): Monad[({type λ[α] = G[F[α]]})#λ] = g $ this 63 | } 64 | 65 | trait LeftComposableMonad[F[_]] extends Monad[F] { 66 | def $ [G[_]](g: Monad[G]): Monad[({type λ[α] = F[G[α]]})#λ] 67 | override def $ [G[_]](g: RightComposableMonad[G]) = $(g.asInstanceOf[Monad[G]]) 68 | } 69 | 70 | trait RightComposableMonad[F[_]] extends Monad[F] { 71 | def & [G[_]](g: Monad[G]): Monad[({type λ[α] = G[F[α]]})#λ] 72 | override def & [G[_]](g: LeftComposableMonad[G]) = &(g.asInstanceOf[Monad[G]]) 73 | } -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/MonadInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import org.scalatest.FlatSpec 4 | import org.scalatest.matchers.ShouldMatchers 5 | import util.{Success, Try} 6 | 7 | class MonadInstancesSpec extends FlatSpec with ShouldMatchers { 8 | behavior of "Built-in monad instances" 9 | 10 | "Options" should "work" in { 11 | context[Option] { 12 | val none: Option[Int] = None 13 | $(42) should equal (Some(42)) 14 | $(Some("abc") + "d") should equal (Some("abcd")) 15 | $(none * 2) should equal (None) 16 | $(Some(5) * Some(3)) should equal (Some(15)) 17 | $(Some(5) * none) should equal (None) 18 | } 19 | } 20 | 21 | "Lists" should "work" in { 22 | context[List] { 23 | $(42) should equal (List(42)) 24 | $(List(1, 2, 3) + 1) should equal (List(2, 3, 4)) 25 | $(List("a", "b") + List("x", "y")) should equal (List("ax", "ay", "bx", "by")) 26 | } 27 | } 28 | 29 | "Tries" should "work" in { 30 | context[Try] { 31 | $(42) should equal (Try(42)) 32 | $(Try(10) * Try(4)) should equal (Success(40)) 33 | val failure = Try(1 / 0) 34 | $(failure + 5) should equal (failure) 35 | $(failure + Try(2 * 2)) should equal (failure) 36 | } 37 | } 38 | 39 | "Futures" should "work" in { 40 | import concurrent.{Await, Future, TimeoutException} 41 | import concurrent.ExecutionContext.Implicits.global 42 | import concurrent.duration._ 43 | context[Future] { 44 | def slowPlus(x: Int, y: Int) = { Thread.sleep(900); x + y } 45 | val a = Future(slowPlus(1, 3)) 46 | val b = Future(slowPlus(2, 4)) 47 | evaluating(Await.result($(a * b + 3), 100 millis)) should produce[TimeoutException] 48 | val c = Future(slowPlus(1, 3)) 49 | val d = Future(slowPlus(2, 4)) 50 | Await.result($(c * d + 3), 1 second) should equal (27) 51 | } 52 | } 53 | 54 | "Streams" should "work" in { 55 | context[Stream] { 56 | $(42) should equal (Stream(42)) 57 | $(Stream(1, 2, 3) + 1) should equal (Stream(2, 3, 4)) 58 | $(Stream("a", "b") + Stream("x", "y")) should equal (Stream("ax", "ay", "bx", "by")) 59 | } 60 | } 61 | 62 | "Lefts" should "work" in { 63 | context(left[String]) { 64 | val l: Either[Int, String] = Left(10) 65 | val l2: Either[String, String] = Left("5") 66 | val r: Either[Int, String] = Right("foo") 67 | $(42) should equal (Left(42)) 68 | $(l + 5) should equal (Left(15)) 69 | $(r - 2) should equal (Right("foo")) 70 | $(l.toString + l2) should equal (Left("105")) 71 | $(l + r) should equal (Right("foo")) 72 | } 73 | } 74 | 75 | "Rights" should "work" in { 76 | context(right[String]) { 77 | val r: Either[String, Int] = Right(10) 78 | val r2: Either[String, Int] = Right(5) 79 | val l: Either[String, Boolean] = Left("foo") 80 | $(42) should equal (Right(42)) 81 | $(r + 2) should equal(Right(12)) 82 | $(l || false) should equal(Left("foo")) 83 | $(r + r2) should equal (Right(15)) 84 | $((r > 5) || l) should equal (Left("foo")) 85 | } 86 | } 87 | 88 | "Ids" should "work" in { 89 | // it does nothing, actually 90 | context(id) { 91 | $(1 + 2) should equal (1 + 2) 92 | } 93 | } 94 | 95 | "Partial functions" should "work" in { 96 | context(partialFunction[Int]) { 97 | val justFoo = $("foo") 98 | justFoo(42) should equal ("foo") 99 | 100 | val foo: PartialFunction[Int, String] = { 101 | case 1 ⇒ "one" 102 | case 2 ⇒ "two" 103 | } 104 | 105 | val fooBang = $(foo + "!") 106 | 107 | fooBang(2) should equal ("two!") 108 | evaluating(fooBang(3)) should produce[MatchError] 109 | 110 | val bar: PartialFunction[Int, String] = { 111 | case 2 ⇒ "deux" 112 | case 3 ⇒ "trois" 113 | } 114 | 115 | val qux = $(foo + bar) 116 | 117 | qux(2) should equal ("twodeux") 118 | evaluating(qux(1)) should produce[MatchError] 119 | } 120 | } 121 | 122 | "Functions" should "work" in { 123 | context(function[String]) { 124 | val chars = (s: String) ⇒ s.length 125 | val letters = (s: String) ⇒ s.count(_.isLetter) 126 | 127 | val nonletters = $(chars - letters) 128 | nonletters("R2-D2") should equal (3) 129 | 130 | val weird = $(chars * 2) 131 | weird("C-3PO") should equal (10) 132 | 133 | val justFive = $(5) 134 | justFive("anything") should equal (5) 135 | } 136 | } 137 | 138 | "Functions of two arguments" should "work" in { 139 | context(function2[String, Char]) { 140 | val append = (s: String) ⇒ (c: Char) ⇒ s + c 141 | val count = (s: String) ⇒ (c: Char) ⇒ s.count(_ == c) 142 | 143 | val foo = $(append + "!") 144 | foo("R2-D2")('2') should equal ("R2-D22!") 145 | 146 | val bar = $(append + count.toString) 147 | bar("R2-D2")('2') should equal ("R2-D222") 148 | 149 | val justTrue = $(true) 150 | justTrue("anything")('X') should equal (true) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/scala/scala/workflow/instances.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import language.higherKinds 4 | import concurrent.{ExecutionContext, Future} 5 | 6 | trait Instances extends FunctorInstances 7 | with SemiIdiomInstances 8 | with IdiomInstances 9 | with SemiMonadInstances 10 | with MonadInstances 11 | with SemigroupInstances 12 | with MonoidInstances 13 | 14 | trait FunctorInstances { 15 | implicit def tupleL[T] = new Functor[({type λ[α] = (α, T)})#λ] { 16 | def map[A, B](f: A ⇒ B) = { case (lhs, rhs) ⇒ (f(lhs), rhs) } 17 | } 18 | 19 | def tuple[T] = tupleL[T] 20 | 21 | implicit def tupleR[T] = new Functor[({type λ[α] = (T, α)})#λ] { 22 | def map[A, B](f: A ⇒ B) = { case (lhs, rhs) ⇒ (lhs, f(rhs)) } 23 | } 24 | 25 | def pair[T] = tupleR[T] 26 | 27 | implicit def tuple3L[M, R] = new Functor[({type λ[α] = (α, M, R)})#λ] { 28 | def map[A, B](f: A ⇒ B) = { case (lhs, mhs, rhs) ⇒ (f(lhs), mhs, rhs) } 29 | } 30 | 31 | implicit def tuple3M[L, R] = new Functor[({type λ[α] = (L, α, R)})#λ] { 32 | def map[A, B](f: A ⇒ B) = { case (lhs, mhs, rhs) ⇒ (lhs, f(mhs), rhs) } 33 | } 34 | 35 | implicit def tuple3R[L, M] = new Functor[({type λ[α] = (L, M, α)})#λ] { 36 | def map[A, B](f: A ⇒ B) = { case (lhs, mhs, rhs) ⇒ (lhs, mhs, f(rhs)) } 37 | } 38 | 39 | implicit def map[T] = new Functor[({type λ[α] = Map[T, α]})#λ] { 40 | def map[A, B](f: A ⇒ B) = _ mapValues f 41 | } 42 | } 43 | 44 | trait SemiIdiomInstances { 45 | val zipList = new SemiIdiom[List] { 46 | def map[A, B](f: A ⇒ B) = _ map f 47 | def app[A, B](fs: List[A ⇒ B]) = _ zip fs map { case (a, f) ⇒ f(a) } 48 | } 49 | } 50 | 51 | trait IdiomInstances { 52 | val zipStream = new Idiom[Stream] { 53 | def point[A](a: ⇒ A) = Stream.continually(a) 54 | def app[A, B](fs: Stream[A ⇒ B]) = _ zip fs map { case (a, f) ⇒ f(a) } 55 | } 56 | } 57 | 58 | trait SemiMonadInstances 59 | 60 | trait MonadInstances { 61 | implicit val option = new RightComposableMonad[Option] { 62 | def point[A](a: ⇒ A) = Option(a) 63 | def bind[A, B](f: A ⇒ Option[B]) = _ flatMap f 64 | def & [G[_]](g: Monad[G]) = new Monad[({type λ[α] = G[Option[α]]})#λ] { 65 | def point[A](a: ⇒ A) = g.point(Option(a)) 66 | def bind[A, B](f: A ⇒ G[Option[B]]) = g.bind { 67 | case Some(a) ⇒ f(a) 68 | case None ⇒ g.point(None) 69 | } 70 | } 71 | } 72 | 73 | implicit val list = new RightComposableMonad[List] { 74 | def point[A](a: ⇒ A) = List(a) 75 | def bind[A, B](f: A ⇒ List[B]) = _ flatMap f 76 | def & [G[_]](g: Monad[G]) = new Monad[({type λ[α] = G[List[α]]})#λ] { 77 | def point[A](a: ⇒ A) = g.point(List(a)) 78 | def bind[A, B](f: A ⇒ G[List[B]]) = g.bind { 79 | _.map(f).fold(g.point(Nil: List[B])) { 80 | (a, b) ⇒ g.app(g.map((x: List[B]) ⇒ (y: List[B]) ⇒ x ++ y)(a))(b) 81 | } 82 | } 83 | } 84 | } 85 | 86 | implicit val try_ = new Monad[util.Try] { 87 | def point[A](a: ⇒ A) = util.Try(a) 88 | def bind[A, B](f: A ⇒ util.Try[B]) = _ flatMap f 89 | } 90 | 91 | implicit def future(implicit executor: ExecutionContext) = new Monad[Future] { 92 | def point[A](a: ⇒ A) = Future(a) 93 | def bind[A, B](f: A ⇒ Future[B]) = _ flatMap f 94 | } 95 | 96 | implicit val stream = new Monad[Stream] { 97 | def point[A](a: ⇒ A) = Stream(a) 98 | def bind[A, B](f: A ⇒ Stream[B]) = _ flatMap f 99 | } 100 | 101 | implicit def left[T] = new RightComposableMonad[({type λ[α] = Either[α, T]})#λ] { 102 | def point[A](a: ⇒ A) = Left(a) 103 | def bind[A, B](f: A ⇒ Either[B, T]) = _.left flatMap f 104 | def & [G[_]](g: Monad[G]) = new Monad[({type λ[α] = G[Either[α, T]]})#λ] { 105 | def point[A](a: ⇒ A) = g.point(Left(a)) 106 | def bind[A, B](f: A ⇒ G[Either[B, T]]) = g.bind { 107 | case Left(a) ⇒ f(a) 108 | case Right(t) ⇒ g.point(Right(t)) 109 | } 110 | } 111 | } 112 | 113 | implicit def right[T] = new RightComposableMonad[({type λ[α] = Either[T, α]})#λ] { 114 | def point[A](a: ⇒ A) = Right(a) 115 | def bind[A, B](f: A ⇒ Either[T, B]) = _.right flatMap f 116 | def & [G[_]](g: Monad[G]) = new Monad[({type λ[α] = G[Either[T, α]]})#λ] { 117 | def point[A](a: ⇒ A) = g.point(Right(a)) 118 | def bind[A, B](f: A ⇒ G[Either[T, B]]) = g.bind { 119 | case Left(t) ⇒ g.point(Left(t)) 120 | case Right(a) ⇒ f(a) 121 | } 122 | } 123 | } 124 | 125 | def either[T] = right[T] 126 | 127 | val id = new RightComposableMonad[({type λ[α] = α})#λ] with LeftComposableMonad[({type λ[α] = α})#λ] { 128 | def point[A](a: ⇒ A) = a 129 | def bind[A, B](f: A ⇒ B) = f 130 | def $ [G[_]](g: Monad[G]) = g 131 | def & [G[_]](g: Monad[G]) = g 132 | } 133 | 134 | implicit def partialFunction[R] = new Monad[({type λ[α] = PartialFunction[R, α]})#λ] { 135 | def point[A](a: ⇒ A) = { case _ ⇒ a } 136 | def bind[A, B](f: A ⇒ PartialFunction[R, B]) = g ⇒ { case r if (g isDefinedAt r) && (f(g(r)) isDefinedAt r) ⇒ f(g(r))(r) } 137 | } 138 | 139 | implicit def function[R] = new LeftComposableMonad[({type λ[α] = R ⇒ α})#λ] { 140 | def point[A](a: ⇒ A) = _ ⇒ a 141 | def bind[A, B](f: A ⇒ R ⇒ B) = g ⇒ r ⇒ f(g(r))(r) 142 | def $ [G[_]](g: Monad[G]) = new Monad[({type λ[α] = R ⇒ G[α]})#λ] { 143 | def point[A](a: ⇒ A) = _ ⇒ g.point(a) 144 | def bind[A, B](f: A ⇒ R ⇒ G[B]) = h ⇒ r ⇒ g.bind((a: A) ⇒ f(a)(r))(h(r)) 145 | } 146 | } 147 | 148 | def reader[E] = function[E] 149 | 150 | implicit def function2[R, S] = new Monad[({type λ[α] = R ⇒ S ⇒ α})#λ] { 151 | def point[A](a: ⇒ A) = _ ⇒ _ ⇒ a 152 | def bind[A, B](f: A ⇒ R ⇒ S ⇒ B) = g ⇒ r ⇒ s ⇒ f(g(r)(s))(r)(s) 153 | } 154 | 155 | implicit def function3[R, S, T] = new Monad[({type λ[α] = R ⇒ S ⇒ T ⇒ α})#λ] { 156 | def point[A](a: ⇒ A) = _ ⇒ _ ⇒ _ ⇒ a 157 | def bind[A, B](f: A ⇒ R ⇒ S ⇒ T ⇒ B) = g ⇒ r ⇒ s ⇒ t ⇒ f(g(r)(s)(t))(r)(s)(t) 158 | } 159 | 160 | implicit def function4[R, S, T, U] = new Monad[({type λ[α] = R ⇒ S ⇒ T ⇒ U ⇒ α})#λ] { 161 | def point[A](a: ⇒ A) = _ ⇒ _ ⇒ _ ⇒ (_: U) ⇒ a 162 | def bind[A, B](f: A ⇒ R ⇒ S ⇒ T ⇒ U ⇒ B) = g ⇒ r ⇒ s ⇒ t ⇒ u ⇒ f(g(r)(s)(t)(u))(r)(s)(t)(u) 163 | } 164 | 165 | implicit def function5[R, S, T, U, V] = new Monad[({type λ[α] = R ⇒ S ⇒ T ⇒ U ⇒ V ⇒ α})#λ] { 166 | def point[A](a: ⇒ A) = _ ⇒ _ ⇒ _ ⇒ _ ⇒ (_: V) ⇒ a 167 | def bind[A, B](f: A ⇒ R ⇒ S ⇒ T ⇒ U ⇒ V ⇒ B) = g ⇒ r ⇒ s ⇒ t ⇒ u ⇒ v ⇒ f(g(r)(s)(t)(u)(v))(r)(s)(t)(u)(v) 168 | } 169 | 170 | implicit def function6[R, S, T, U, V, W] = new Monad[({type λ[α] = R ⇒ S ⇒ T ⇒ U ⇒ V ⇒ W ⇒ α})#λ] { 171 | def point[A](a: ⇒ A) = _ ⇒ _ ⇒ _ ⇒ _ ⇒ _ ⇒ _ ⇒ a 172 | def bind[A, B](f: A ⇒ R ⇒ S ⇒ T ⇒ U ⇒ V ⇒ W ⇒ B) = g ⇒ r ⇒ s ⇒ t ⇒ u ⇒ v ⇒ w ⇒ f(g(r)(s)(t)(u)(v)(w))(r)(s)(t)(u)(v)(w) 173 | } 174 | 175 | def state[S] = new Monad[({type λ[α] = S ⇒ (α, S)})#λ] { 176 | def point[A](a: ⇒ A) = (a, _) 177 | def bind[A, B](f: A ⇒ S ⇒ (B, S)) = Function.uncurried(f).tupled.compose 178 | } 179 | 180 | def accumulator[O : Monoid] = new RightComposableMonad[({type λ[α] = (α, O)})#λ] { 181 | private val monoid = implicitly[Monoid[O]] 182 | def point[A](a: ⇒ A) = (a, monoid.unit) 183 | def bind[A, B](f: A ⇒ (B, O)) = { 184 | case (a, o) ⇒ pair[B].map(monoid.append(o, _: O))(f(a)) 185 | } 186 | def & [G[_]](g: Monad[G]) = new Monad[({type λ[α] = G[(α, O)]})#λ] { 187 | def point[A](a: ⇒ A) = g.point((a, monoid.unit)) 188 | def bind[A, B](f: A ⇒ G[(B, O)]) = g.bind { 189 | case (a, o) ⇒ g.map { 190 | pair[B].map(monoid.append(o, _: O)) 191 | }(f(a)) 192 | } 193 | } 194 | } 195 | 196 | implicit def cont[R] = new Monad[({type λ[α] = (α ⇒ R) ⇒ R})#λ] { 197 | def point[A](a: ⇒ A) = _(a) 198 | def bind[A, B](f: A ⇒ (B ⇒ R) ⇒ R) = g ⇒ h ⇒ g(f(_)(h)) 199 | } 200 | } 201 | 202 | trait SemigroupInstances { 203 | val maxSemigroup = Semigroup[Int](_ max _) 204 | 205 | val minSemigroup = Semigroup[Int](_ min _) 206 | } 207 | 208 | trait MonoidInstances { 209 | implicit def listMonoid[A] = Monoid[List[A]](Nil, _ ++ _) 210 | 211 | val conjunctionMonoid = Monoid[Boolean](true, _ && _) 212 | 213 | val disjunctionMonoid = Monoid[Boolean](false, _ || _) 214 | 215 | implicit val additionMonoid = Monoid[Int](0, _ + _) 216 | 217 | val multiplicationMonoid = Monoid[Int](1, _ * _) 218 | 219 | implicit val string = Monoid[String]("", _ + _) 220 | 221 | implicit def functionMonoid[A] = Monoid[Function[A, A]](identity[A], _ compose _) 222 | } -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/WorkflowContextSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import org.scalatest.FlatSpec 4 | import org.scalatest.matchers.ShouldMatchers 5 | 6 | class WorkflowContextSpec extends FlatSpec with ShouldMatchers { 7 | behavior of "Workflow context" 8 | 9 | val one: Option[Int] = Some(1) 10 | val two: Option[Int] = Some(2) 11 | val three: Option[Int] = Some(3) 12 | val four: Option[Int] = Some(4) 13 | val five: Option[Int] = Some(5) 14 | val six: Option[Int] = Some(6) 15 | val seven: Option[Int] = Some(7) 16 | val eight: Option[Int] = Some(8) 17 | val nine: Option[Int] = Some(9) 18 | val ten: Option[Int] = Some(10) 19 | val none: Option[Int] = None 20 | 21 | val foo: Option[String] = Some("foo") 22 | val snone: Option[String] = None 23 | 24 | def divide(x: Double, y: Double) = if (y == 0) None else Some(x / y) 25 | 26 | context[Option] { 27 | it should "lift object operator application" in { 28 | $(ten - three) should equal (seven) 29 | $(three - none) should equal (None) 30 | } 31 | 32 | it should "lift method application" in { 33 | def minus(a: Int, b: Int) = a - b 34 | $(minus(ten, three)) should equal (seven) 35 | $(minus(three, none)) should equal (None) 36 | } 37 | 38 | it should "lift curried method application" in { 39 | def minus(a: Int)(b: Int) = a - b 40 | $(minus(ten)(three)) should equal (seven) 41 | $(minus(three)(none)) should equal (None) 42 | } 43 | 44 | it should "lift function application" in { 45 | val minus = (a: Int, b: Int) ⇒ a - b 46 | $(minus(ten, three)) should equal (seven) 47 | $(minus(three, none)) should equal (None) 48 | } 49 | 50 | it should "lift curried function application" in { 51 | val minus = (a: Int) ⇒ (b: Int) ⇒ a - b 52 | $(minus(ten)(three)) should equal (seven) 53 | $(minus(three)(none)) should equal (None) 54 | } 55 | 56 | it should "lift inner value method call" in { 57 | $(foo.reverse) should equal (Some("oof")) 58 | $(snone.reverse) should equal (None) 59 | $(ten.toString) should equal (Some("10")) 60 | $(none.toString) should equal (None) 61 | } 62 | 63 | it should "lift partially lifted object operator application" in { 64 | $(ten - 2) should equal (eight) 65 | $(none - 2) should equal (None) 66 | $(14 - ten) should equal (four) 67 | $(14 - none) should equal (None) 68 | } 69 | 70 | it should "lift partially lifted method application" in { 71 | def minus(a: Int, b: Int) = a - b 72 | $(minus(ten, 2)) should equal (eight) 73 | $(minus(none, 2)) should equal (None) 74 | $(minus(12, ten)) should equal (two) 75 | $(minus(12, none)) should equal (None) 76 | } 77 | 78 | it should "lift partially lifted curried method application" in { 79 | def minus(a: Int)(b: Int) = a - b 80 | $(minus(ten)(2)) should equal (eight) 81 | $(minus(none)(2)) should equal (None) 82 | $(minus(12)(ten)) should equal (two) 83 | $(minus(12)(none)) should equal (None) 84 | } 85 | 86 | it should "lift partially lifted function application" in { 87 | val minus = (a: Int, b: Int) ⇒ a - b 88 | $(minus(ten, 2)) should equal (eight) 89 | $(minus(none, 2)) should equal (None) 90 | $(minus(12, ten)) should equal (two) 91 | $(minus(12, none)) should equal (None) 92 | } 93 | 94 | it should "lift partially lifted curried function application" in { 95 | val minus = (a: Int) ⇒ (b: Int) ⇒ a - b 96 | $(minus(ten)(2)) should equal (eight) 97 | $(minus(none)(2)) should equal (None) 98 | $(minus(12)(ten)) should equal (two) 99 | $(minus(12)(none)) should equal (None) 100 | } 101 | 102 | it should "lift compound funcall with all the arguments lifted" in { 103 | $(ten - (six / three)) should equal (eight) 104 | $(ten - (none / three)) should equal (None) 105 | $((ten - four) / three) should equal (two) 106 | $((ten - none) / three) should equal (None) 107 | } 108 | 109 | it should "lift compound funcall with some of the arguments non-lifted" in { 110 | $(ten - (six / 2)) should equal (seven) 111 | $(ten - (none / 2)) should equal (None) 112 | $(ten - (18 / six)) should equal (seven) 113 | $(ten - (18 / none)) should equal (None) 114 | $(2 * (ten - six)) should equal (eight) 115 | $(2 * (ten - none)) should equal (None) 116 | $((ten - six) * 3) should equal (Some(12)) 117 | $((ten - none) * 3) should equal (None) 118 | $((ten - 4) / six) should equal (one) 119 | $((ten - 4) / none) should equal (None) 120 | $((1 + ten) * six) should equal (Some(66)) 121 | $((1 + ten) * none) should equal (None) 122 | } 123 | 124 | it should "lift function itself" in { 125 | val f: Option[Int ⇒ Int] = Some(_ + 1) 126 | val g: Option[Int ⇒ Int] = None 127 | 128 | $(f(ten)) should equal (Some(11)) 129 | $(f(none)) should equal (None) 130 | $(g(ten)) should equal (None) 131 | $(g(none)) should equal (None) 132 | } 133 | 134 | it should "lift object fields" in { 135 | case class Employee(name: String, boss: Option[Employee], workPlace: Option[WorkPlace]) 136 | case class WorkPlace(cubicle: Int) 137 | 138 | val steve = Employee("Steve", None, Some(WorkPlace(100))) 139 | val john = Employee("John", Some(steve), Some(WorkPlace(410))) 140 | val bob = Employee("Bob", Some(steve), None) 141 | 142 | $(steve.workPlace.cubicle + 100) should equal (Some(200)) 143 | $(bob.workPlace.cubicle + 100) should equal (None) 144 | 145 | $(john.boss.workPlace.cubicle + 100) should equal (Some(200)) 146 | $(steve.boss.workPlace.cubicle + 100) should equal (None) 147 | } 148 | 149 | it should "lift block with a single statement" in { 150 | $ { 151 | ten - (six / 2) 152 | } should equal (seven) 153 | $ { 154 | ten - (none / 2) 155 | } should equal (None) 156 | } 157 | 158 | it should "monadically lift block with several valdefs" in { 159 | $ { 160 | val a = six 161 | val b = a - four 162 | val c = ten / b 163 | 15 / c 164 | } should equal (three) 165 | 166 | $ { 167 | val a = six 168 | val b = a - four 169 | val c = ten / b 170 | divide(15, c) 171 | } should equal (three) 172 | 173 | $ { 174 | val a = six 175 | val b = 6 - 4 176 | val c = ten / b 177 | divide(15, c) 178 | } should equal (three) 179 | 180 | $ { 181 | val a = six 182 | val b = a - four 183 | val c = ten / b 184 | 15 / 5 185 | } should equal (three) 186 | } 187 | 188 | it should "monadically lift dependent subexpression" in { 189 | $(divide(divide(four, one), divide(ten, five))) should equal (two) 190 | $(divide(divide(four, one), divide(none, five))) should equal (none) 191 | } 192 | } 193 | 194 | it should "build workflow context from explicitly passed workflow instance" in { 195 | context(list) { 196 | $(List(1, 2, 3) * 2) should equal (List(2, 4, 6)) 197 | $(List("a", "b") + List("x", "y")) should equal (List("ax", "ay", "bx", "by")) 198 | } 199 | } 200 | 201 | it should "resolve workflow context from passed workflow type" in { 202 | $[Option](ten - (six / 2)) should equal (seven) 203 | context[Option] { 204 | $[List](List(1, 2, 3) * 2) should equal (List(2, 4, 6)) // disregard enclosing context block 205 | } 206 | } 207 | 208 | it should "build workflow context for workflows composition" in { 209 | context(list $ option) { 210 | val xs = List(two, three, None) 211 | val ys = List(None, four, five) 212 | 213 | $(xs * 10) should equal (List(Some(20), Some(30), None)) 214 | $(xs + ys) should equal (List(None, six, seven, None, seven, eight, None)) 215 | } 216 | 217 | context(list $ option $ option) { 218 | val xs = List(Some(None), Some(two), None) 219 | val ys = List(None, Some(None), Some(five)) 220 | 221 | $(xs + 3) should equal (List(Some(None), Some(five), None)) 222 | $(xs * ys) should equal (List(Some(None), None, Some(None), Some(ten), None)) 223 | } 224 | } 225 | 226 | it should "lift nested blocks with nested scopes" in { 227 | workflow[Option] { 228 | val a = ten 229 | val b = { 230 | val a = six 231 | val b = four 232 | a - b 233 | } 234 | a / b 235 | } should equal (five) 236 | } 237 | 238 | // it should "rewrite if-s" in { 239 | // def run(a: Int) = 240 | // workflow[Option] { 241 | // val b = 6 242 | // if (a > seven) { 243 | // val c = two 244 | // b * c 245 | // } else 246 | // b - 2 247 | // } 248 | // 249 | // run(8) should equal (Some(12)) 250 | // run(6) should equal (four) 251 | // } 252 | } 253 | -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/ReadmeSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import language.higherKinds 4 | import language.postfixOps 5 | 6 | import org.scalatest.FlatSpec 7 | import org.scalatest.matchers.ShouldMatchers 8 | 9 | /** Auxilary spec to be sure that examples in Readme file are working */ 10 | class ReadmeSpec extends FlatSpec with ShouldMatchers { 11 | behavior of "Examples from Readme file" 12 | 13 | def divide(x: Double, y: Double) = if (y == 0) None else Some(x / y) 14 | 15 | "Examples from 'Quick start' section" should "be correct" in { 16 | context[Option] { 17 | $(Some(42) + 1) should equal (Some(43)) 18 | $(Some(10) + Some(5) * Some(2)) should equal (Some(20)) 19 | } 20 | 21 | context[List] { 22 | $(List(1, 2, 3) * 2) should equal (List(2, 4, 6)) 23 | $(List("a", "b") + List("x", "y")) should equal (List("ax", "ay", "bx", "by")) 24 | } 25 | 26 | context(zipList) { 27 | $(List(1, 2, 3, 4) * List(2, 3, 4)) should equal (List(2, 6, 12)) 28 | } 29 | 30 | context(map[String]) { 31 | $(Map("foo" → 10, "bar" → 5) * 2) should equal (Map("foo" → 20, "bar" → 10)) 32 | } 33 | 34 | context(function[String]) { 35 | val chars = (s: String) ⇒ s.length 36 | val letters = (s: String) ⇒ s.count(_.isLetter) 37 | val nonletters = $(chars - letters) 38 | nonletters("R2-D2") should equal (3) 39 | } 40 | 41 | context[Option] { 42 | $ { 43 | val x = divide(1, 2) 44 | val y = divide(4, x) 45 | divide(y, x) 46 | } should equal (Some(16)) 47 | } 48 | 49 | workflow[Option] { 50 | val x = divide(1, 2) 51 | val y = divide(4, x) 52 | divide(y, x) 53 | } should equal (Some(16)) 54 | } 55 | 56 | "Example from 'Rules of rewriting' section" should "be correct" in { 57 | context(option) { 58 | $(2 * 3 + Some(10) * Some(5)) should equal (option.app(option.map((x$1: Int) ⇒ (x$2: Int) ⇒ 2 * 3 + x$1 * x$2)(Some(10)))(Some(5))) 59 | 60 | $(42) should equal (option.point(42)) 61 | $(Some(42) + 1) should equal (option.map((x$1: Int) ⇒ x$1 + 1)(Some(42))) 62 | $(Some(2) * Some(3)) should equal (option.app(option.map((x$1: Int) ⇒ (x$2: Int) ⇒ x$1 * x$2)(Some(2)))(Some(3))) 63 | $(divide(1.5, 2)) should equal (divide(1.5, 2)) 64 | $(divide(Some(1.5), 2)) should equal (option.bind((x$1: Double) ⇒ divide(x$1, 2))(Some(1.5))) 65 | $(divide(Some(1.5), Some(2))) should equal (option.bind((x$1: Double) ⇒ option.bind((x$2: Int) ⇒ divide(x$1, x$2))(Some(2)))(Some(1.5))) 66 | $(divide(Some(1.5), 2) + 1) should equal (option.bind((x$1: Double) ⇒ option.map((x$2: Double) ⇒ x$2 + 1)(divide(x$1, 2)))(Some(1.5))) 67 | } 68 | } 69 | 70 | "Examples from 'Syntax of workflows'" should "be correct" in { 71 | val x = context[List] { 72 | $(List(2, 5) * List(3, 7)) 73 | } 74 | 75 | val y = context(list) { 76 | $(List(2, 5) * List(3, 7)) 77 | } 78 | 79 | val z = $[List](List(2, 5) * List(3, 7)) 80 | 81 | val t = workflow(list) { List(2, 5) * List(3, 7) } 82 | 83 | x should equal (y) 84 | y should equal (z) 85 | t should equal (z) 86 | } 87 | 88 | "Example from 'Composition of workflows'" should "be correct" in { 89 | context(list $ option) { 90 | $(List(Some(2), Some(3), None) * 10) should equal (List(Some(20), Some(30), None)) 91 | } 92 | } 93 | 94 | sealed trait Expr 95 | case class Var(id: String) extends Expr 96 | case class Val(value: Int) extends Expr 97 | case class Add(lhs: Expr, rhs: Expr) extends Expr 98 | 99 | val testExpr1 = Add(Var("x"), Var("y")) 100 | val testExpr2 = Add(Var("x"), Var("z")) 101 | 102 | type Env = Map[String, Int] 103 | val env = Map("x" → 42, "y" → 6) 104 | 105 | def fetch(x: String)(env: Env): Option[Int] = env.get(x) 106 | 107 | "Regular eval" should "be correct" in { 108 | def eval(expr: Expr)(env: Env): Option[Int] = 109 | expr match { 110 | case Var(x) ⇒ fetch(x)(env) 111 | case Val(value) ⇒ Some(value) 112 | case Add(x, y) ⇒ for { 113 | lhs ← eval(x)(env) 114 | rhs ← eval(y)(env) 115 | } yield lhs + rhs 116 | } 117 | 118 | eval(testExpr1)(env) should equal (Some(48)) 119 | eval(testExpr2)(env) should equal (None) 120 | } 121 | 122 | "Workflow eval" should "be correct" in { 123 | def eval: Expr ⇒ Env ⇒ Option[Int] = 124 | context(function[Env] $ option) { 125 | case Var(x) ⇒ fetch(x) 126 | case Val(value) ⇒ $(value) 127 | case Add(x, y) ⇒ $(eval(x) + eval(y)) 128 | } 129 | 130 | eval(testExpr1)(env) should equal (Some(48)) 131 | eval(testExpr2)(env) should equal (None) 132 | } 133 | 134 | trait Cell[T] { 135 | def ! : T 136 | def := (value: T) { throw new UnsupportedOperationException } 137 | } 138 | 139 | val frp = new Idiom[Cell] { 140 | def point[A](a: ⇒ A) = new Cell[A] { 141 | private var value = a 142 | override def := (a: A) { value = a } 143 | def ! = value 144 | } 145 | def app[A, B](f: Cell[A ⇒ B]) = a ⇒ new Cell[B] { 146 | def ! = f!(a!) 147 | } 148 | } 149 | 150 | "FRP example" should "be correct" in { 151 | context(frp) { 152 | val a = $(10) 153 | val b = $(5) 154 | 155 | val c = $(a + b * 2) 156 | 157 | (c!) should equal (20) 158 | 159 | b := 7 160 | 161 | (c!) should equal (24) 162 | } 163 | } 164 | 165 | "Stack language interpretation" should "be correct" in { 166 | type Stack = List[Int] 167 | type State = Either[String, Stack] 168 | 169 | val stackLang = state[State] 170 | 171 | def command(f: Stack ⇒ State) = (st: State) ⇒ ((), either[String].bind(f)(st)) 172 | 173 | def put(value: Int) = command { 174 | case stack ⇒ Right(value :: stack) 175 | } 176 | 177 | def dup = command { 178 | case a :: stack ⇒ Right(a :: a :: stack) 179 | case _ ⇒ Left("Stack underflow while executing `dup`") 180 | } 181 | 182 | def rot = command { 183 | case a :: b :: stack ⇒ Right(b :: a :: stack) 184 | case _ ⇒ Left("Stack underflow while executing `rot`") 185 | } 186 | 187 | def sub = command { 188 | case a :: b :: stack ⇒ Right((b - a) :: stack) 189 | case _ ⇒ Left("Stack underflow while executing `sub`") 190 | } 191 | 192 | def execute(program: State ⇒ (Unit, State)) = { 193 | val (_, state) = program(Right(Nil)) 194 | state 195 | } 196 | 197 | context(stackLang) { 198 | val programA = $ { put(5); dup; put(7); rot; sub } 199 | execute(programA) should equal(Right(List(2, 5))) 200 | 201 | val programB = $ { put(5); dup; sub; rot; dup } 202 | execute(programB) should equal(Left("Stack underflow while executing `rot`")) 203 | } 204 | } 205 | 206 | "Point-free notation examples" should "be correct" in { 207 | context(function[Char]) { 208 | val isLetter: Char ⇒ Boolean = _.isLetter 209 | val isDigit: Char ⇒ Boolean = _.isDigit 210 | 211 | // Traditionally 212 | val isLetterOrDigit = (ch: Char) ⇒ isLetter(ch) || isDigit(ch) 213 | 214 | // Combinatorially 215 | val isLetterOrDigit2 = $(isLetter || isDigit) 216 | 217 | isLetterOrDigit2('X') should equal (true) 218 | isLetterOrDigit2('2') should equal (true) 219 | isLetterOrDigit2('-') should equal (false) 220 | } 221 | 222 | context(function[Double]) { 223 | val sqrt: Double ⇒ Double = x ⇒ math.sqrt(x) 224 | val sqr: Double ⇒ Double = x ⇒ x * x 225 | val log: Double ⇒ Double = x ⇒ math.log(x) 226 | 227 | // Traditionally 228 | val f = (x: Double) ⇒ sqrt((sqr(x) - 1) / (sqr(x) + 1)) 229 | 230 | // Combinatorially 231 | val f2 = sqrt compose $((sqr - 1) / (sqr + 1)) 232 | 233 | f2(5) should equal (f(5)) 234 | 235 | // Traditionally 236 | val g = (x: Double) ⇒ (sqr(log(x)) - 1) / (sqr(log(x)) + 1) 237 | 238 | // Combinatorially 239 | val g2 = log andThen $((sqr - 1) / (sqr + 1)) 240 | 241 | g2(10) should equal (g(10)) 242 | } 243 | } 244 | 245 | "Purely functional logging" should "work" in { 246 | val logging = accumulator[List[String]] 247 | 248 | def mult(x: Int, y: Int) = (x * y, List(s"Calculating $x * $y")) 249 | 250 | def info(message: String) = (Unit, List(message)) 251 | 252 | val (result, log) = workflow(logging) { 253 | info("Lets define a variable") 254 | val x = 2 255 | 256 | info("And calculate a square of it") 257 | val square = mult(x, x) 258 | 259 | info("Also a cube and add them together") 260 | val cube = mult(mult(x, x), x) 261 | val sum = square + cube 262 | 263 | info("This is all so silly") 264 | sum / 2 265 | } 266 | 267 | result should equal (6) 268 | log should equal (List("Lets define a variable", 269 | "And calculate a square of it", 270 | "Calculating 2 * 2", 271 | "Also a cube and add them together", 272 | "Calculating 2 * 2", 273 | "Calculating 4 * 2", 274 | "This is all so silly")) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/test/scala/scala/workflow/CompositionSpec.scala: -------------------------------------------------------------------------------- 1 | package scala.workflow 2 | 3 | import language.higherKinds 4 | 5 | import org.scalatest.FlatSpec 6 | import org.scalatest.matchers.ShouldMatchers 7 | 8 | /** Specification for rules of workflow composition */ 9 | class CompositionSpec extends FlatSpec with ShouldMatchers { 10 | behavior of "Rules of workflow composition" 11 | 12 | import CompositionSpec._ 13 | 14 | /* Type arguments of workflow instances can only be checked at compile 15 | time (unless Manifests or TypeTags are used) due to type erasure. 16 | Successful compilation of this specification is enough to prove 17 | that everything is working. */ 18 | 19 | "Composition of two functors" should "produce a functor" in { 20 | functor.f $ functor.g: Functor[({type λ[α] = F[G[α]]})#λ] 21 | functor.f & functor.g: Functor[({type λ[α] = G[F[α]]})#λ] 22 | } 23 | 24 | "Composition of two semi-idioms" should "produce a semi-idiom" in { 25 | semiidiom.f $ semiidiom.g: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 26 | semiidiom.f & semiidiom.g: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 27 | } 28 | 29 | "Composition of two idioms" should "produce an idiom" in { 30 | idiom.f $ idiom.g: Idiom[({type λ[α] = F[G[α]]})#λ] 31 | idiom.f & idiom.g: Idiom[({type λ[α] = G[F[α]]})#λ] 32 | } 33 | 34 | "Composition of two non-composable monads" should "produce an idiom" in { 35 | monad.f $ monad.g: Idiom[({type λ[α] = F[G[α]]})#λ] 36 | monad.f & monad.g: Idiom[({type λ[α] = G[F[α]]})#λ] 37 | } 38 | 39 | "Composition of a functor and a semi-idiom" should "produce a functor" in { 40 | functor.f $ semiidiom.g: Functor[({type λ[α] = F[G[α]]})#λ] 41 | functor.f & semiidiom.g: Functor[({type λ[α] = G[F[α]]})#λ] 42 | semiidiom.g $ functor.f: Functor[({type λ[α] = G[F[α]]})#λ] 43 | semiidiom.g & functor.f: Functor[({type λ[α] = F[G[α]]})#λ] 44 | } 45 | 46 | "Composition of a functor and an idiom" should "produce a functor" in { 47 | functor.f $ idiom.g: Functor[({type λ[α] = F[G[α]]})#λ] 48 | functor.f & idiom.g: Functor[({type λ[α] = G[F[α]]})#λ] 49 | idiom.g $ functor.f: Functor[({type λ[α] = G[F[α]]})#λ] 50 | idiom.g & functor.f: Functor[({type λ[α] = F[G[α]]})#λ] 51 | } 52 | 53 | "Composition of a functor and a semi-monad" should "produce a functor" in { 54 | functor.f $ semimonad.g: Functor[({type λ[α] = F[G[α]]})#λ] 55 | functor.f & semimonad.g: Functor[({type λ[α] = G[F[α]]})#λ] 56 | semimonad.g $ functor.f: Functor[({type λ[α] = G[F[α]]})#λ] 57 | semimonad.g & functor.f: Functor[({type λ[α] = F[G[α]]})#λ] 58 | } 59 | 60 | "Composition of a functor and a monad" should "produce a functor" in { 61 | functor.f $ monad.g: Functor[({type λ[α] = F[G[α]]})#λ] 62 | functor.f & monad.g: Functor[({type λ[α] = G[F[α]]})#λ] 63 | monad.g $ functor.f: Functor[({type λ[α] = G[F[α]]})#λ] 64 | monad.g & functor.f: Functor[({type λ[α] = F[G[α]]})#λ] 65 | } 66 | 67 | "Composition of a semi-idiom and an idiom" should "produce a semi-idiom" in { 68 | semiidiom.f $ idiom.g: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 69 | semiidiom.f & idiom.g: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 70 | idiom.g $ semiidiom.f: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 71 | idiom.g & semiidiom.f: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 72 | } 73 | 74 | "Composition of a semi-idiom and a semi-monad" should "produce a semi-idiom" in { 75 | semiidiom.f $ semimonad.g: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 76 | semiidiom.f & semimonad.g: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 77 | semimonad.g $ semiidiom.f: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 78 | semimonad.g & semiidiom.f: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 79 | } 80 | 81 | "Composition of a semi-idiom and a monad" should "produce a semi-idiom" in { 82 | semiidiom.f $ monad.g: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 83 | semiidiom.f & monad.g: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 84 | monad.g $ semiidiom.f: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 85 | monad.g & semiidiom.f: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 86 | } 87 | 88 | "Composition of a idiom and a semi-monad" should "produce a semi-idiom" in { 89 | idiom.f $ semimonad.g: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 90 | idiom.f & semimonad.g: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 91 | semimonad.g $ idiom.f: SemiIdiom[({type λ[α] = G[F[α]]})#λ] 92 | semimonad.g & idiom.f: SemiIdiom[({type λ[α] = F[G[α]]})#λ] 93 | } 94 | 95 | "Composition of a idiom and a monad" should "produce an idiom" in { 96 | idiom.f $ monad.g: Idiom[({type λ[α] = F[G[α]]})#λ] 97 | idiom.f & monad.g: Idiom[({type λ[α] = G[F[α]]})#λ] 98 | monad.g $ idiom.f: Idiom[({type λ[α] = G[F[α]]})#λ] 99 | monad.g & idiom.f: Idiom[({type λ[α] = F[G[α]]})#λ] 100 | } 101 | 102 | "Composition of a semi-monad and a monad" should "produce a semi-monad" in { 103 | idiom.f $ monad.g: Idiom[({type λ[α] = F[G[α]]})#λ] 104 | idiom.f & monad.g: Idiom[({type λ[α] = G[F[α]]})#λ] 105 | monad.g $ idiom.f: Idiom[({type λ[α] = G[F[α]]})#λ] 106 | monad.g & idiom.f: Idiom[({type λ[α] = F[G[α]]})#λ] 107 | } 108 | 109 | "Composition of a left-composable semi-monad and a semi-monad" should "produce a semi-monad" in { 110 | semimonad.left.f $ semimonad.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 111 | semimonad.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 112 | semimonad.left.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 113 | semimonad.left.f $ semimonad.left.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 114 | semimonad.right.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 115 | semimonad.left.f $ semimonad.right.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 116 | } 117 | 118 | "Composition of a semi-monad and a right-composable semi-monad" should "produce a semi-monad" in { 119 | semimonad.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 120 | semimonad.right.f & semimonad.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 121 | semimonad.right.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 122 | semimonad.right.f & semimonad.right.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 123 | semimonad.left.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 124 | semimonad.right.f & semimonad.left.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 125 | } 126 | 127 | "Composition of a left-composable semi-monad and a monad" should "produce a semi-monad" in { 128 | semimonad.left.f $ monad.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 129 | monad.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 130 | semimonad.left.f $ monad.left.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 131 | monad.left.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 132 | semimonad.left.f $ monad.right.g: SemiMonad[({type λ[α] = F[G[α]]})#λ] 133 | monad.right.g & semimonad.left.f: SemiMonad[({type λ[α] = F[G[α]]})#λ] 134 | } 135 | 136 | "Composition of a monad and a right-composable semi-monad" should "produce a semi-monad" in { 137 | semimonad.right.f & monad.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 138 | monad.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 139 | semimonad.right.f & monad.right.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 140 | monad.right.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 141 | semimonad.right.f & monad.left.g: SemiMonad[({type λ[α] = G[F[α]]})#λ] 142 | monad.left.g $ semimonad.right.f: SemiMonad[({type λ[α] = G[F[α]]})#λ] 143 | } 144 | 145 | "Composition of a left-composable monad and a monad" should "produce a monad" in { 146 | monad.left.f $ monad.g: Monad[({type λ[α] = F[G[α]]})#λ] 147 | monad.g & monad.left.f: Monad[({type λ[α] = F[G[α]]})#λ] 148 | monad.left.g & monad.left.f: Monad[({type λ[α] = F[G[α]]})#λ] 149 | monad.left.f $ monad.left.g: Monad[({type λ[α] = F[G[α]]})#λ] 150 | monad.right.g & monad.left.f: Monad[({type λ[α] = F[G[α]]})#λ] 151 | monad.left.f $ monad.right.g: Monad[({type λ[α] = F[G[α]]})#λ] 152 | } 153 | 154 | "Composition of a monad and a right-composable monad" should "produce a monad" in { 155 | monad.g $ monad.right.f: Monad[({type λ[α] = G[F[α]]})#λ] 156 | monad.right.f & monad.g: Monad[({type λ[α] = G[F[α]]})#λ] 157 | monad.right.g $ monad.right.f: Monad[({type λ[α] = G[F[α]]})#λ] 158 | monad.right.f & monad.right.g: Monad[({type λ[α] = G[F[α]]})#λ] 159 | monad.left.g $ monad.right.f: Monad[({type λ[α] = G[F[α]]})#λ] 160 | monad.right.f & monad.left.g: Monad[({type λ[α] = G[F[α]]})#λ] 161 | } 162 | } 163 | 164 | object CompositionSpec { 165 | case class F[A](a: A) 166 | case class G[A](a: A) 167 | 168 | object functor { 169 | val f = new Functor[F] { 170 | def map[A, B](h: A ⇒ B) = f ⇒ F(h(f.a)) 171 | } 172 | 173 | val g = new Functor[G] { 174 | def map[A, B](h: A ⇒ B) = g ⇒ G(h(g.a)) 175 | } 176 | } 177 | 178 | object semiidiom { 179 | val f = new SemiIdiom[F] { 180 | def map[A, B](h: A ⇒ B) = f ⇒ F(h(f.a)) 181 | def app[A, B](h: F[A ⇒ B]) = f ⇒ F(h.a(f.a)) 182 | } 183 | 184 | val g = new SemiIdiom[G] { 185 | def map[A, B](h: A ⇒ B) = g ⇒ G(h(g.a)) 186 | def app[A, B](h: G[A ⇒ B]) = g ⇒ G(h.a(g.a)) 187 | } 188 | } 189 | 190 | object idiom { 191 | val f = new Idiom[F] { 192 | def point[A](a: ⇒ A) = F(a) 193 | def app[A, B](h: F[A ⇒ B]) = f ⇒ F(h.a(f.a)) 194 | } 195 | 196 | val g = new Idiom[G] { 197 | def point[A](a: ⇒ A) = G(a) 198 | def app[A, B](h: G[A ⇒ B]) = g ⇒ G(h.a(g.a)) 199 | } 200 | } 201 | 202 | object semimonad { 203 | val f = new SemiMonad[F] { 204 | def map[A, B](h: A ⇒ B) = f ⇒ F(h(f.a)) 205 | def app[A, B](h: F[A ⇒ B]) = f ⇒ F(h.a(f.a)) 206 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 207 | } 208 | 209 | val g = new SemiMonad[G] { 210 | def map[A, B](h: A ⇒ B) = g ⇒ G(h(g.a)) 211 | def app[A, B](h: G[A ⇒ B]) = g ⇒ G(h.a(g.a)) 212 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 213 | } 214 | 215 | object left { 216 | val f = new LeftComposableSemiMonad[F] { s ⇒ 217 | def map[A, B](h: A ⇒ B) = f ⇒ F(h(f.a)) 218 | def app[A, B](h: F[A ⇒ B]) = f ⇒ F(h.a(f.a)) 219 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 220 | def $[H[_]](h: SemiMonad[H]) = new SemiMonad[({type λ[α] = F[H[α]]})#λ] { 221 | def map[A, B](g: A ⇒ B) = s map (h map g) 222 | def app[A, B](g: F[H[A ⇒ B]]) = s app (s map h.app[A, B])(g) 223 | def bind[A, B](f: A ⇒ F[H[B]]) = { 224 | case F(ha) ⇒ F(h.bind((a: A) ⇒ f(a).a)(ha)) 225 | } 226 | } 227 | } 228 | 229 | val g = new LeftComposableSemiMonad[G] { s ⇒ 230 | def map[A, B](h: A ⇒ B) = g ⇒ G(h(g.a)) 231 | def app[A, B](h: G[A ⇒ B]) = g ⇒ G(h.a(g.a)) 232 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 233 | def $[H[_]](h: SemiMonad[H]) = new SemiMonad[({type λ[α] = G[H[α]]})#λ] { 234 | def map[A, B](f: A ⇒ B) = s map (h map f) 235 | def app[A, B](f: G[H[A ⇒ B]]) = s app (s map h.app[A, B])(f) 236 | def bind[A, B](g: A ⇒ G[H[B]]) = { 237 | case G(ha) ⇒ G(h.bind((a: A) ⇒ g(a).a)(ha)) 238 | } 239 | } 240 | } 241 | } 242 | 243 | object right { 244 | val f = new RightComposableSemiMonad[F] { s ⇒ 245 | def map[A, B](h: A ⇒ B) = f ⇒ F(h(f.a)) 246 | def app[A, B](h: F[A ⇒ B]) = f ⇒ F(h.a(f.a)) 247 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 248 | def &[H[_]](h: SemiMonad[H]) = new SemiMonad[({type λ[α] = H[F[α]]})#λ] { 249 | def map[A, B](g: A ⇒ B) = h map (s map g) 250 | def app[A, B](g: H[F[A ⇒ B]]) = h app (h map s.app[A, B])(g) 251 | def bind[A, B](f: A ⇒ H[F[B]]) = h.bind { fa ⇒ f(fa.a) } 252 | } 253 | } 254 | 255 | val g = new RightComposableSemiMonad[G] { s ⇒ 256 | def map[A, B](h: A ⇒ B) = g ⇒ G(h(g.a)) 257 | def app[A, B](h: G[A ⇒ B]) = g ⇒ G(h.a(g.a)) 258 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 259 | def &[H[_]](h: SemiMonad[H]) = new SemiMonad[({type λ[α] = H[G[α]]})#λ] { 260 | def map[A, B](f: A ⇒ B) = h map (s map f) 261 | def app[A, B](f: H[G[A ⇒ B]]) = h app (h map s.app[A, B])(f) 262 | def bind[A, B](g: A ⇒ H[G[B]]) = h.bind { ga ⇒ g(ga.a) } 263 | } 264 | } 265 | } 266 | } 267 | 268 | object monad { 269 | val f = new Monad[F] { 270 | def point[A](a: ⇒ A) = F(a) 271 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 272 | } 273 | 274 | val g = new Monad[G] { 275 | def point[A](a: ⇒ A) = G(a) 276 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 277 | } 278 | 279 | object left { 280 | val f = new LeftComposableMonad[F] { 281 | def point[A](a: ⇒ A) = F(a) 282 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 283 | def $[H[_]](h: Monad[H]) = new Monad[({type λ[α] = F[H[α]]})#λ] { 284 | def point[A](a: ⇒ A) = F(h.point(a)) 285 | def bind[A, B](f: A ⇒ F[H[B]]) = { 286 | case F(ha) ⇒ F(h.bind((a: A) ⇒ f(a).a)(ha)) 287 | } 288 | } 289 | } 290 | 291 | val g = new LeftComposableMonad[G] { 292 | def point[A](a: ⇒ A) = G(a) 293 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 294 | def $[H[_]](h: Monad[H]) = new Monad[({type λ[α] = G[H[α]]})#λ] { 295 | def point[A](a: ⇒ A) = G(h.point(a)) 296 | def bind[A, B](g: A ⇒ G[H[B]]) = { 297 | case G(ha) ⇒ G(h.bind((a: A) ⇒ g(a).a)(ha)) 298 | } 299 | } 300 | } 301 | } 302 | 303 | object right { 304 | val f = new RightComposableMonad[F] { 305 | def point[A](a: ⇒ A) = F(a) 306 | def bind[A, B](h: A ⇒ F[B]) = f ⇒ h(f.a) 307 | def &[H[_]](h: Monad[H]) = new Monad[({type λ[α] = H[F[α]]})#λ] { 308 | def point[A](a: ⇒ A) = h.point(F(a)) 309 | def bind[A, B](f: A ⇒ H[F[B]]) = h.bind { fa ⇒ f(fa.a) } 310 | } 311 | } 312 | 313 | val g = new RightComposableMonad[G] { 314 | def point[A](a: ⇒ A) = G(a) 315 | def bind[A, B](h: A ⇒ G[B]) = g ⇒ h(g.a) 316 | def &[H[_]](h: Monad[H]) = new Monad[({type λ[α] = H[G[α]]})#λ] { 317 | def point[A](a: ⇒ A) = h.point(G(a)) 318 | def bind[A, B](g: A ⇒ H[G[B]]) = h.bind { ga ⇒ g(ga.a) } 319 | } 320 | } 321 | } 322 | } 323 | } -------------------------------------------------------------------------------- /src/main/scala/scala/workflow/package.scala: -------------------------------------------------------------------------------- 1 | package scala 2 | 3 | import language.experimental.macros 4 | import language.higherKinds 5 | import reflect.macros.{TypecheckException, Context} 6 | import util.{Failure, Success} 7 | 8 | package object workflow extends Instances { 9 | def context[F[_]](code: _): _ = macro contextImpl 10 | def contextImpl(c: Context)(code: c.Tree): c.Tree = { 11 | import c.universe._ 12 | 13 | val Apply(TypeApply(_, List(typeTree)), _) = c.macroApplication 14 | 15 | c.macroApplication.updateAttachment(contextFromType(c)(typeTree)) 16 | 17 | code 18 | } 19 | 20 | /** Untyped macro functions are not allowed to be overloaded. There is a 21 | * workaround -- we can defined `context` as a function and an `apply` 22 | * method of `context` object. 23 | */ 24 | object context { 25 | def apply(workflow: Any)(code: _): _ = macro contextImpl 26 | def contextImpl(c: Context)(workflow: c.Expr[Any])(code: c.Tree): c.Tree = { 27 | import c.universe._ 28 | 29 | val Expr(instance) = workflow 30 | 31 | c.macroApplication.updateAttachment(contextFromTerm(c)(instance)) 32 | 33 | code 34 | } 35 | } 36 | 37 | def workflow[F[_]](code: _): _ = macro workflowImpl 38 | def workflowImpl(c: Context)(code: c.Tree): c.Tree = { 39 | import c.universe._ 40 | 41 | val Apply(TypeApply(_, List(typeTree)), _) = c.macroApplication 42 | 43 | val workflowContext = contextFromType(c)(typeTree) 44 | 45 | rewrite(c)(code, workflowContext).asInstanceOf[Tree] 46 | } 47 | 48 | /** Just like with `context`, we imitate function overloading with `apply` 49 | * method of `workflow` object. 50 | */ 51 | object workflow { 52 | def apply(workflow: Any)(code: _): _ = macro workflowImpl 53 | def workflowImpl(c: Context)(workflow: c.Expr[Any])(code: c.Tree): c.Tree = { 54 | import c.universe._ 55 | 56 | val Expr(instance) = workflow 57 | 58 | val workflowContext = contextFromTerm(c)(instance) 59 | 60 | rewrite(c)(code, workflowContext).asInstanceOf[Tree] 61 | } 62 | } 63 | 64 | def $[F[_]](code: _): _ = macro $impl 65 | def $impl(c: Context)(code: c.Tree): c.Tree = { 66 | import c.universe._ 67 | 68 | val Apply(TypeApply(_, List(typeTree: TypeTree)), _) = c.macroApplication 69 | 70 | val workflowContext = if (typeTree.original != null) 71 | contextFromType(c)(typeTree) 72 | else 73 | contextFromEnclosure(c) 74 | 75 | rewrite(c)(code, workflowContext).asInstanceOf[Tree] 76 | } 77 | 78 | private def contextFromType(c: Context)(typeTree: c.Tree) = { 79 | import c.universe._ 80 | 81 | val tpe = typeTree.tpe 82 | 83 | val typeRef = TypeRef(NoPrefix, typeOf[Workflow[Any]].typeSymbol, List(tpe)) 84 | val instance = c.inferImplicitValue(typeRef) 85 | 86 | if (instance == EmptyTree) 87 | c.abort(typeTree.pos, s"Unable to find $typeRef instance in implicit scope") 88 | 89 | WorkflowContext(tpe, instance) 90 | } 91 | 92 | private def contextFromTerm(c: Context)(instance: c.Tree): WorkflowContext = { 93 | import c.universe._ 94 | 95 | val workflowSymbol = instance.tpe.baseClasses find (_.fullName == "scala.workflow.Workflow") getOrElse { 96 | c.abort(instance.pos, "Not a workflow instance") 97 | } 98 | 99 | val TypeRef(_, _, List(tpe)) = instance.tpe.baseType(workflowSymbol) 100 | 101 | WorkflowContext(tpe, instance) 102 | } 103 | 104 | private def contextFromEnclosure(c: Context) = { 105 | val workflowContext = for { 106 | context ← c.openMacros.view 107 | attachments = context.macroApplication.attachments 108 | workflowContext ← attachments.get[WorkflowContext] 109 | } yield workflowContext 110 | 111 | workflowContext.headOption getOrElse { 112 | c.abort(c.enclosingPosition, "Workflow brackets outside of `context' block") 113 | } 114 | } 115 | 116 | private def rewrite(c: Context)(code: c.Tree, workflowContext: WorkflowContext): c.Tree = { 117 | import c.universe._ 118 | 119 | val WorkflowContext(workflow: Type, instance: Tree) = workflowContext 120 | 121 | val interfaces = instance.tpe.baseClasses map (_.fullName) 122 | def assertImplements(interface: String) { 123 | if (!interfaces.contains(interface)) 124 | c.abort(c.enclosingPosition, s"Enclosing workflow for type $workflow does not implement $interface") 125 | } 126 | 127 | def resolveLiftedType(tpe: Type): Option[Type] = 128 | tpe.baseType(workflow.typeSymbol) match { 129 | case baseType @ TypeRef(_, _, typeArgs) ⇒ 130 | workflow match { 131 | case PolyType(List(wildcard), typeRef: TypeRef) ⇒ 132 | // When workflow is passed as type lambda, we need to take the type 133 | // from wildcard position, so we zip through both typerefs to seek for a substitution 134 | def findSubstitution(wildcardedType: Type, concreteType: Type): Option[Type] = { 135 | if (wildcardedType.typeSymbol == wildcard) 136 | Some(concreteType) 137 | else 138 | (wildcardedType, concreteType) match { 139 | case (wctpe: TypeRef, ctpe: TypeRef) ⇒ 140 | wctpe.args zip ctpe.args find { 141 | case (wct, at) ⇒ !(wct =:= at) 142 | } flatMap { 143 | case (wct, at) ⇒ findSubstitution(wct, at) 144 | } 145 | case _ ⇒ None 146 | } 147 | } 148 | findSubstitution(typeRef, baseType) 149 | case _ ⇒ 150 | // This only works for type constructor of one argument 151 | // TODO: provide implementation for n-arity type constructors 152 | typeArgs.headOption 153 | } 154 | case _ ⇒ None 155 | } 156 | 157 | case class Bind(name: TermName, tpt: TypeTree, value: Tree) { 158 | def isUsedIn(frame: Frame) = frame exists ((_: Bind).value exists (_ equalsStructure q"$name")) 159 | } 160 | type Frame = List[Bind] 161 | class Scope(val materialized: List[Frame], val frames: List[Frame]) { 162 | def materialize(bind: Bind) = new Scope(materialized.init :+ (materialized.last :+ bind), frames) 163 | def :+ (bind: Bind) = new Scope(materialized, (bind :: frames.head) :: frames.tail) 164 | def ++ (binds: List[Bind]) = new Scope(materialized, (frames.head ++ binds) :: frames.tail) 165 | def enter = new Scope(materialized :+ Nil, Nil :: frames) 166 | def leave = new Scope(materialized.init, frames.tail) 167 | def local = materialized.flatten ++ frames.flatten 168 | def wrapping(tree: Tree) = local.foldLeft(tree) { 169 | case (expr, bind) ⇒ q"{ val ${bind.name}: ${bind.tpt} = ???; $expr }" 170 | } 171 | } 172 | object Scope { 173 | val empty = new Scope(List(Nil), List(Nil)) 174 | def merge(scopes: List[Scope]) = { 175 | val materialized = scopes map (_.materialized) 176 | val frames = scopes map (_.frames) 177 | new Scope(mergeFrames(materialized), mergeFrames(frames)) 178 | } 179 | def merge(scopes: Scope*): Scope = merge(scopes.toList) 180 | private def mergeFrames(frames: List[List[Frame]]) = frames.map(_.head).flatten.distinct :: frames.head.tail 181 | } 182 | 183 | def typeCheck(tree: Tree, scope: Scope): util.Try[Tree] = 184 | util.Try(c.typeCheck(scope wrapping tree.duplicate)) recoverWith { 185 | case e: TypecheckException 186 | if e.msg.contains("follow this method with `_'") || e.msg.contains("ambiguous reference") || 187 | (e.msg.contains("package") && e.msg.contains("is not a value")) ⇒ Success(EmptyTree) 188 | case e: TypecheckException 189 | if e.msg.contains("missing arguments for constructor") ⇒ 190 | util.Try(c.typeCheck(scope wrapping q"(${tree.duplicate})(_)")) recover { 191 | case e: TypecheckException 192 | if !e.msg.contains("too many arguments for constructor") ⇒ EmptyTree 193 | } 194 | } 195 | 196 | /* This whole function stinks. It's long, unreliable and some looks redundant. 197 | * TODO: Obviously need to refactor it at some point */ 198 | def rewriteBlock(scope: Scope): Block ⇒ (Scope, Tree) = { 199 | case Block(stats, result) ⇒ 200 | def contExpr(scope: Scope, signature: Option[(Modifiers, TermName, Tree)], expr: Tree) = { 201 | val (newscope, newexpr) = rewrite(scope.enter)(expr) 202 | val frame = newscope.materialized.last 203 | if (frame.isEmpty) { 204 | val name = signature.fold(TermName("_")) { 205 | case (_, name, _) ⇒ name 206 | } 207 | val tpe = typeCheck(newexpr, newscope).get.tpe 208 | val bind = Bind(name, TypeTree(tpe), newexpr) 209 | val cont = (_: Boolean) ⇒ signature.fold(block(q"$newexpr")) { 210 | case (mods, _, tpt) ⇒ block(ValDef(mods, name, tpt, newexpr)) 211 | } 212 | 213 | val newerscope = if (signature.isDefined) scope :+ bind else scope 214 | 215 | (newerscope ++ newscope.frames.head, cont) 216 | } else { 217 | val value = apply(frame)(newexpr) 218 | val tpe = typeCheck(newexpr, newscope).get.tpe 219 | val name = signature.fold(TermName("_")) { 220 | case (_, name, _) ⇒ name 221 | } 222 | val bind = Bind(name, TypeTree(tpe), value) 223 | val cont = (x: Boolean) ⇒ if (x) >>=(bind) compose lambda(bind) // Especially dirty hack! 224 | else map(bind) compose lambda(bind) // TODO: figure out a better way 225 | 226 | val newerscope = if (signature.isDefined) scope :+ bind else scope 227 | 228 | (newerscope ++ newscope.frames.head, cont) 229 | } 230 | } 231 | 232 | def contRewrite(scope: Scope): Tree ⇒ (Scope, Boolean ⇒ Tree ⇒ Tree) = { 233 | case ValDef(mods, name, tpt, expr) ⇒ 234 | contExpr(scope, Some((mods, name, tpt)), expr) 235 | 236 | case expr ⇒ 237 | contExpr(scope, None, expr) 238 | } 239 | 240 | val (newscope, cont) = stats.foldLeft((scope, (x: Boolean) ⇒ (t: Tree) ⇒ t)) { 241 | (acc, stat) ⇒ 242 | val (scope, cont) = acc 243 | val (newscope, newcont) = contRewrite(scope)(stat) 244 | (newscope, (x: Boolean) ⇒ cont(true) compose newcont(x)) 245 | } 246 | 247 | val (newerscope, newresult) = rewrite(newscope.enter)(result) 248 | val frame = newerscope.materialized.last 249 | val value = if (frame.isEmpty) cont(false)(newresult) else cont(true)(apply(frame)(newresult)) 250 | (newerscope.leave, value) 251 | } 252 | 253 | // def rewriteIf(scope: Scope): If ⇒ (Scope, Tree) = { 254 | // case If(condition, consequent, alternative) if alternative != Literal(Constant(())) ⇒ 255 | // val (newscope, newcondition) = rewrite(scope)(condition) 256 | // val (consscope, newconsequent) = rewrite(newscope)(consequent) 257 | // val (altscope, newalternative) = rewrite(newscope)(alternative) 258 | // (Scope.merge(consscope, altscope), If(newcondition, newconsequent, newalternative)) 259 | // case expr ⇒ 260 | // c.abort(expr.pos, "`if` expressions with missing alternative are not supported") 261 | // } 262 | 263 | def rewriteExpr(scope: Scope): Tree ⇒ (Scope, Tree) = { 264 | case expr @ (_ : Literal | _ : Ident | _ : New) ⇒ extractBinds(scope, expr) 265 | 266 | case Apply(fun, args) ⇒ 267 | val (funscope, newfun) = rewrite(scope)(fun) 268 | val (argsscopes, newargs) = args.map(rewrite(funscope)).unzip 269 | extractBinds(Scope.merge(argsscopes), q"$newfun(..$newargs)") 270 | 271 | case Select(value, method) ⇒ 272 | val (newscope, newvalue) = rewrite(scope)(value) 273 | extractBinds(newscope, q"$newvalue.$method") 274 | } 275 | 276 | def rewrite(scope: Scope): Tree ⇒ (Scope, Tree) = { 277 | case block: Block ⇒ rewriteBlock(scope)(block) 278 | 279 | // case condition: If ⇒ rewriteIf(scope)(condition) 280 | 281 | case expr @ (_ : Literal | _ : Ident | _ : New | _ : Apply | _ : Select) ⇒ rewriteExpr(scope)(expr) 282 | 283 | case expr ⇒ c.abort(expr.pos, "Unsupported expression") 284 | } 285 | 286 | def extractBinds(scope: Scope, expr: Tree) = 287 | typeCheck(expr, scope) match { 288 | case Success(tpt) ⇒ 289 | resolveLiftedType(tpt.tpe) match { 290 | case Some(tpe) ⇒ 291 | val name = TermName(c.freshName("arg$")) 292 | val bind = Bind(name, TypeTree(tpe), expr) 293 | (scope materialize bind, q"$name") 294 | 295 | case None ⇒ (scope, expr) 296 | } 297 | case Failure(e) ⇒ 298 | val binds = scope.materialized.flatten map { 299 | case Bind(name, tpt, value) ⇒ s" $name: $tpt <- $value" 300 | } 301 | val message = s"Type error during rewriting of expression within $workflow context" 302 | val bindsList = if (binds.isEmpty) "" else s" where${binds mkString ("\n\n", "\n", "\n\n")}" 303 | c.abort(c.enclosingPosition, s"${e.getMessage}\n\n $expr\n\n$bindsList$message") 304 | } 305 | 306 | def lambda(bind: Bind): Tree ⇒ Tree = { 307 | expr ⇒ q"(${bind.name}: ${bind.tpt}) ⇒ $expr" 308 | } 309 | 310 | def isIdentity: Tree ⇒ Boolean = { 311 | case Function(List(arg), Ident(ref)) if arg.name == ref ⇒ true 312 | case _ ⇒ false 313 | } 314 | 315 | def block(tree: Tree): Tree ⇒ Tree = { 316 | expr ⇒ q"{ $tree; $expr }" 317 | } 318 | 319 | def point: Tree ⇒ Tree = { 320 | assertImplements("scala.workflow.Pointing") 321 | expr ⇒ q"$instance.point($expr)" 322 | } 323 | 324 | def map(bind: Bind): Tree ⇒ Tree = { 325 | assertImplements("scala.workflow.Mapping") 326 | expr ⇒ if (isIdentity(expr)) bind.value else q"$instance.map($expr)(${bind.value})" 327 | } 328 | 329 | def app(bind: Bind): Tree ⇒ Tree = { 330 | assertImplements("scala.workflow.Applying") 331 | expr ⇒ q"$instance.app($expr)(${bind.value})" 332 | } 333 | 334 | def >>=(bind: Bind): Tree ⇒ Tree = { 335 | assertImplements("scala.workflow.Binding") 336 | expr ⇒ q"$instance.bind($expr)(${bind.value})" 337 | } 338 | 339 | def apply: Frame ⇒ Tree ⇒ Tree = { 340 | case Nil ⇒ point 341 | case bind :: Nil ⇒ map(bind) compose lambda(bind) 342 | case bind :: binds ⇒ 343 | if (bind isUsedIn binds) 344 | >>=(bind) compose lambda(bind) compose apply(binds) 345 | else 346 | app(bind) compose apply(binds) compose lambda(bind) 347 | } 348 | 349 | /* Blocks and expressions are rewritten a bit differently 350 | * (block don't have materialized binds afterwards), hence two branches */ 351 | code match { 352 | case block: Block ⇒ 353 | val (_, expr) = rewrite(Scope.empty)(block) 354 | expr 355 | case _ ⇒ 356 | val (scope, expr) = rewrite(Scope.empty)(code) 357 | apply(scope.materialized.flatten)(expr) 358 | } 359 | } 360 | } 361 | 362 | package workflow { 363 | private[workflow] case class WorkflowContext(tpe: Any, instance: Any) 364 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Scala workflow [![Travis CI Status](https://api.travis-ci.org/aztek/scala-workflow.png)](https://travis-ci.org/aztek/scala-workflow) 2 | ============== 3 | `scala-workflow` helps to nicely organize applicative and monadic computations 4 | in Scala with 2.11 macros, resembling _`for`-comprehension_ and some enhanced 5 | version of _idiom brackets_. 6 | 7 | `scala-workflow` only requires [untyped macros](http://docs.scala-lang.org/overviews/macros/untypedmacros.html) 8 | that is an experimental feature of [Macro Paradise](http://docs.scala-lang.org/overviews/macros/paradise.html). 9 | 10 | ``` 11 | $ git clone https://github.com/aztek/scala-workflow 12 | $ cd scala-workflow 13 | $ sbt console 14 | import scala.workflow._ 15 | Welcome to Scala version 2.11.0 16 | Type in expressions to have them evaluated. 17 | Type :help for more information. 18 | 19 | scala> $[List](List(1, 2) * List(4, 5)) 20 | res0: List[Int] = List(4, 5, 8, 10) 21 | ``` 22 | 23 | Contents 24 | -------- 25 | * [Quick start](#quick-start) 26 | * [Workflows](#workflows) 27 | * [Hierarchy of workflows](#hierarchy-of-workflows) 28 | * [Rewriting rules](#rewriting-rules) 29 | * [Context definition](#context-definition) 30 | * [Composing workflows](#composing-workflows) 31 | * [Examples](#examples) 32 | * [Evaluator for a language of expressions](#evaluator-for-a-language-of-expressions) 33 | * [Functional reactive programming](#functional-reactive-programming) 34 | * [Monadic interpreter for stack programming language](#monadic-interpreter-for-stack-programming-language) 35 | * [Point-free notation](#point-free-notation) 36 | * [Purely functional logging](#purely-functional-logging) 37 | * [Disclaimer](#disclaimer) 38 | 39 | Quick start 40 | ----------- 41 | Import `workflow` interface. 42 | 43 | ```scala 44 | import workflow._ 45 | ``` 46 | 47 | You will now have three macros at hand — `context`, `$` and `workflow`. Wrap a 48 | block of code in `context` and the argument of `$` will begin to act funny. 49 | 50 | ```scala 51 | context[Option] { 52 | $(Some(42) + 1) should equal (Some(43)) 53 | $(Some(10) + Some(5) * Some(2)) should equal (Some(20)) 54 | } 55 | 56 | context[List] { 57 | $(List(1, 2, 3) * 2) should equal (List(2, 4, 6)) 58 | $(List("a", "b") + List("x", "y")) should equal (List("ax", "ay", "bx", "by")) 59 | } 60 | ``` 61 | 62 | Enclosing `context` macro takes type constructor `F[_]` as an argument and 63 | makes all the `$`s inside evaluate their arguments as if everything of type 64 | `F[T]` there was of type `T`, except it retains _computational effects_, 65 | associated with `F`. 66 | 67 | The exact rules of evaluation are defined in an object, that extends special 68 | `scala.workflow.Workflow[F[_]]` trait. Such objects can either be implicitly 69 | summoned by type constructor name (like the ones shown above), or passed 70 | explicitly, like `zipList` below. 71 | 72 | ```scala 73 | context(zipList) { 74 | $(List(1, 2, 3, 4) * List(2, 3, 4)) should equal (List(2, 6, 12)) 75 | } 76 | ``` 77 | 78 | There are numerous of `Workflow[F[_]]` objects you can find in `instances.scala` 79 | file. All of them are instances of _functors_, _applicative functors_ (also 80 | called _idioms_), _monads_ and a couple of other intermediate algebraic 81 | structures. 82 | 83 | ```scala 84 | context(map[String]) { 85 | $(Map("foo" → 10, "bar" → 5) * 2) should equal (Map("foo" → 20, "bar" → 10)) 86 | } 87 | 88 | context(function[String]) { 89 | val chars = (s: String) ⇒ s.length 90 | val letters = (s: String) ⇒ s.count(_.isLetter) 91 | val nonletters = $(chars - letters) 92 | nonletters("R2-D2") should equal (3) 93 | } 94 | ``` 95 | 96 | You can pass complex blocks of code to `$`. 97 | 98 | ```scala 99 | def divide(x: Double, y: Double) = if (y == 0) None else Some(x / y) 100 | 101 | context[Option] { 102 | $ { 103 | val x = divide(1, 2) 104 | val y = divide(4, x) 105 | divide(y, x) 106 | } should equal (Some(16)) 107 | } 108 | ``` 109 | 110 | Nested `context` and `$` calls might look awkward, so instead you can use 111 | special syntactic sugar, called `workflow`. 112 | 113 | ```scala 114 | workflow[Option] { 115 | val x = divide(1, 2) 116 | val y = divide(4, x) 117 | divide(y, x) 118 | } should equal (Some(16)) 119 | ``` 120 | 121 | Just like in `context`, you can pass either type constructor or workflow 122 | object. 123 | 124 | Workflows 125 | --------- 126 | The goal of `scala-workflow` is to provide boilerplate-free syntax for 127 | computations with effects, encoded with monads and idioms. _Workflow_ 128 | abstracts the concept of computation in effectful context. 129 | 130 | Instances of `Workflow` trait provide methods, that are used for desugaring 131 | code in correspondent effectful contexts. The more methods an instance has, 132 | the more powerful it is, and the richer language features can be used. 133 | 134 | The ultimate goal is to support the whole set of Scala language features. For 135 | now, however, only literals, function applications and `val` definitions are 136 | supported. But development of the project is still in progress and you are very 137 | welcome to contribute. 138 | 139 | ### Hierarchy of workflows 140 | The hierarchy of workflows is built around an empty `Workflow[F[_]]` trait and 141 | several derived traits, that add methods to it. So far there are four of them: 142 | 143 | ```scala 144 | trait Pointing[F[_]] extends Workflow[F] { 145 | def point[A](a: ⇒ A): F[A] 146 | } 147 | 148 | trait Mapping[F[_]] extends Workflow[F] { 149 | def map[A, B](f: A ⇒ B): F[A] ⇒ F[B] 150 | } 151 | 152 | trait Applying[F[_]] extends Workflow[F] with Mapping[F] { 153 | def app[A, B](f: F[A ⇒ B]): F[A] ⇒ F[B] 154 | } 155 | 156 | trait Binding[F[_]] extends Workflow[F] { 157 | def bind[A, B](f: A ⇒ F[B]): F[A] ⇒ F[B] 158 | } 159 | ``` 160 | 161 | Each method corresponds to a particular feature of workflow context. 162 | - `point` allows to put pure value inside the workflow. It is only generated 163 | when you call `$(a)` for some pure `a`. 164 | 165 | - `map` is used to map over one lifted value in an expression. This is the 166 | same `map` you can find in Scalas `List`, `Option` and other classes. 167 | 168 | - `app` is used to map over more that one independently lifted values within a 169 | context. "Independently" means that you can evaluate lifted arguments in any 170 | order. Example of an expression with lifted values that depend on each other: 171 | `$(divide(divide(1, 2), 3))` (with `divide` definition taken from "Quick start" 172 | section). Both `divide` calls are lifted, but we can only evaluate outmost 173 | `divide` after the inner one has been evaluated. Note, that `app` cannot be used 174 | without `map`, hence the inheritance of the traits. 175 | 176 | - `bind` is used to desugar expressions with arbitrary many arbitrary dependent 177 | lifted values. 178 | 179 | You can define workflow instances simply by mixing above-mentioned traits, or 180 | using one of predefined shortcuts, representing commonly used algebraic 181 | structures. 182 | 183 | ```scala 184 | trait Functor[F[_]] extends Mapping[F] 185 | 186 | trait SemiIdiom[F[_]] extends Functor[F] with Applying[F] 187 | 188 | trait Idiom[F[_]] extends SemiIdiom[F] with Pointing[F] { 189 | def map[A, B](f: A ⇒ B) = app(point(f)) 190 | } 191 | 192 | trait SemiMonad[F[_]] extends SemiIdiom[F] with Binding[F] 193 | 194 | trait Monad[F[_]] extends Idiom[F] with Binding[F] { 195 | def app[A, B](f: F[A ⇒ B]) = bind(a ⇒ bind((g: A ⇒ B) ⇒ point(g(a)))(f)) 196 | } 197 | ``` 198 | 199 | Note, that `Functor`/`Idiom`/`Monad` is merely a shortcut. You are not required 200 | to implement any of it particularly to be able to use workflow contexts. They 201 | are mostly convenient, because have some of the methods already implemented and 202 | can be [composed](#composing-workflows). 203 | 204 | ### Rewriting rules 205 | One important difference of `scala-workflow` from similar syntactic extension 206 | is that it always require the least powerful interface of a workflow instance 207 | for generated code. That means, that you can have idiom brackets kind of syntax 208 | for functors (such as `Map[A, B]`) and `for`/`do`-notation kind of syntax for 209 | monads without `return` (they are called `SemiMonad`s here). 210 | 211 | Current implementation uses untyped macros and takes untyped Scala AST as an 212 | argument. Then we start by eagerly typechecking all the subexpressions (i.e., 213 | starting with the most nested subexpressions) and find, which of them 214 | typechecks successfully with the result type corresponding to the type of the 215 | workflow. If those are found, they are being replaces with their non-lifted 216 | counterparts, and the whole thing starts over again, until the whole expression 217 | typechecks correctly and there is a list of lifted values at hand. 218 | 219 | Consider the example below. 220 | 221 | ```scala 222 | context(option) { 223 | $(2 * 3 + Some(10) * Some(5)) 224 | } 225 | ``` 226 | 227 | All the numbers typecheck and are not inside `Option`, so they are left as is. 228 | `2 * 3` also typechecks to `Int` and is left as is. `Some(10)` and `Some(5)` 229 | both typechecks to `Option[Int]` (well, technically, `Some[Int]`, but we can 230 | handle that), so both arguments are lifted. 231 | 232 | Generated code will look like this. 233 | 234 | ```scala 235 | option.app( 236 | option.map( 237 | (x$1: Int) ⇒ (x$2: Int) ⇒ 238 | 2 * 3 + x$1 * x$2 239 | )(Some(10)) 240 | )(Some(5)) 241 | ``` 242 | 243 | Special analysis takes care of dependencies between lifted values to be sure to 244 | produce `bind` instead of `app` where needed. 245 | 246 | Here are some of other examples of code rewriting within `Option` context. 247 | 248 | #### Simple expressions 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 260 | 261 | 262 | 266 | 270 | 271 | 272 | 273 | 280 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 297 | 302 | 303 | 304 | 305 | 312 | 318 | 319 | 320 | 321 | 328 | 333 | 334 |
Inside the $Generated codePure Scala counterpart
$(42)
option.point(42)
Some(42)
259 |
$(Some(42) + 1)
option.map(
263 |   (x$1: Int) ⇒
264 |     x$1 + 1
265 | )(Some(42))
for {
267 |   x ← Some(42)
268 | } yield x + 1
269 |
$(Some(2) * Some(3))
option.app(
274 |   option.map(
275 |     (x$1: Int) ⇒
276 |       (x$2: Int) ⇒
277 |         x$1 * x$2
278 |   )(Some(2))
279 | )(Some(3))
for {
281 |   x ← Some(2)
282 |   y ← Some(3)
283 | } yield x * y
284 |
$(divide(1, 2))
divide(1, 2)
divide(1, 2)
$(divide(Some(1.5), 2))
option.bind(
294 |   (x$1: Double) ⇒
295 |     divide(x$1, 2)
296 | )(Some(1.5))
for {
298 |   x ← Some(1.5)
299 |   y ← divide(x, 2)
300 | } yield y
301 |
$(divide(Some(1.5), Some(2)))
option.bind(
306 |   (x$1: Double) ⇒
307 |     option.bind(
308 |       (x$2: Int) ⇒
309 |         divide(x$1, x$2)
310 |     )(Some(2))
311 | )(Some(1.5))
for {
313 |   x ← Some(1.5)
314 |   y ← Some(2)
315 |   z ← divide(x, y)
316 | } yield z
317 |
$(divide(Some(1.5), 2) + 1)
option.bind(
322 |   (x$1: Double) ⇒
323 |     option.map(
324 |       (x$2: Double) ⇒
325 |         x$2 + 1
326 |     )(divide(x$1, 2))
327 | )(Some(1.5))
for {
329 |   x ← Some(1.5)
330 |   y ← divide(x, 2)
331 | } yield y + 1
332 |
335 | 336 | #### Blocks of code 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 348 | 352 | 355 | 356 | 357 | 362 | 369 | 373 | 374 | 375 | 380 | 385 | 389 | 390 | 391 | 395 | 402 | 407 | 408 |
Inside the $Generated codePure Scala counterpart
$ {
345 |   val x = Some(10)
346 |   x + 2
347 | }
option.map(
349 |   (x$1: Int) ⇒
350 |     x$1 + 2
351 | )(Some(10))
for {
353 |   x ← Some(10)
354 | } yield x + 2
$ {
358 |   val x = Some(10)
359 |   val y = Some(5)
360 |   x + y
361 | }
option.bind(
363 |   (x$1: Int) ⇒
364 |     option.map(
365 |       (x$2: Int) ⇒
366 |         x$1 + x$2
367 |     )(Some(5))
368 | )(Some(10))
for {
370 |   x ← Some(10)
371 |   y ← Some(5)
372 | } yield x + y
$ {
376 |   val x = Some(10)
377 |   val y = x − 3
378 |   x * y
379 | }
option.map(
381 |   (x$1: Int) ⇒
382 |     val y = x$1 − 3
383 |     x$1 * y
384 | )(Some(10))
for {
386 |   x ← Some(10)
387 |   y = x - 3
388 | } yield x * y
$(2 + {
392 |   val x = Some(10)
393 |   x * 2
394 | })
option.map(
396 |   (x$1: Int) ⇒
397 |     2 + x$1
398 | )(option.map(
399 |   (x$2: Int) ⇒
400 |     x$2 * 2
401 | )(Some(10)))
for {
403 |   y ← for {
404 |     x ← Some(10)
405 |   } yield x * 2
406 | } yield 2 + y
409 | 410 | ### Context definition 411 | Workflow context is defined with `context` macro that either takes a workflow 412 | instance as an argument, or a type constructor `F[_]`, such that there is some 413 | workflow instance defined somewhere in the implicits scope. 414 | 415 | The following examples are equivalent. 416 | 417 | ```scala 418 | context[List] { 419 | $(List(2, 5) * List(3, 7)) 420 | } 421 | 422 | context(list) { 423 | $(List(2, 5) * List(3, 7)) 424 | } 425 | ``` 426 | 427 | Macro `$` takes workflow context from the closest `context` block. 428 | Alternatively, you can provide type constructor, whose workflow instance will 429 | be taken from the implicits scope. 430 | 431 | ```scala 432 | $[List](List(2, 5) * List(3, 7)) 433 | ``` 434 | 435 | That way, `$` will disregard any enclosing `context` block and will work within 436 | `Workflow[List]` context. 437 | 438 | Nested applications of `context` and `$` can be replaced with `workflow` macro. 439 | You are encouraged to do so for complex blocks of code. You are discourage to 440 | use `$` inside. `workflow` either takes a workflow instance as an argument, or 441 | a type constructor `F[_]` and rewrites the block of code in the same way as `$`. 442 | 443 | ```scala 444 | workflow(list) { List(2, 5) * List(3, 7) } 445 | ``` 446 | 447 | There are plenty of built-in workflow instances in traits `FunctorInstances`, 448 | `SemiIdiomInstances`, `IdiomInstances` and `MonadInstances`. They are all 449 | mixed to the package object of `scala.workflow`, so once you have `workflow._` 450 | imported, you get access to all of them. Alternatively, you can import just the 451 | macros `import workflow.{context, workflow, $}` and access workflow instances 452 | from `Functor`, `SemiIdiom`, `Idiom` and `Monad` objects. 453 | 454 | ### Composing workflows 455 | Any pair of functors, semi-idioms or idioms can be composed. That is, for any 456 | type constructors `F[_]` and `G[_]` one can build instances of `Functor[F[G[_]]]` 457 | and `Functor[G[F[_]]]` with instances of `Functor[F]` and `Functor[G]` (the same 458 | for semi-idioms and idioms). 459 | 460 | The syntax for workflow composition is `f $ g`, where `f` and `g` are functor, 461 | semi-idiom or idiom instances of type constructors `F[_]` and `G[_]`. The result 462 | would be, correspondingly, functor, semi-idiom or idiom of type constructor 463 | `F[G[_]]`. 464 | 465 | ```scala 466 | context(list $ option) { 467 | $(List(Some(2), Some(3), None) * 10) should equal (List(Some(20), Some(30), None)) 468 | } 469 | ``` 470 | 471 | You can also combine workflows of different classes with the same syntax, the 472 | result workflow will implement the weaker interface of the two. For instance, 473 | `map[String] $ option` will implement `Functor`, because `map`s `Functor` is 474 | weaker than `option`s `Monad`. 475 | 476 | `$` method has a counterpart method `&`, that produces `G[F[_]]` workflow 477 | instance. Naturally, `f $ g = g & f`. 478 | 479 | Monads and semi-monads in general cannot be composed. Instead, some particular 480 | monads can provide their own specific implementation of `$` or `&` method. 481 | However, even having, say, `$` method defined, monad might not be able to 482 | define `&`. That is, `Monad[F]` can either construct `Monad[F[G[_]]` or 483 | `Monad[G[F[_]]]` with arbitrary `Monad[G]`. 484 | 485 | To capture this notion, `Monad[F]` can be extended to either 486 | `LeftComposableMonad[F]` or `RightComposableMonad[F]`. In the first case it is 487 | supposed to be able to produce `Monad[F[G[_]]]` and therefore implement `$` 488 | method. In the second case it is supposed to be able to produce `Monad[G[F[_]]]` 489 | and therefore implement `$` method. 490 | 491 | For example, `Option` is _right-composable_, i.e. can implement 492 | `def & [G[_]](g: Monad[G]): Monad[G[Option[_]]]`, whereas 493 | function monad `R ⇒ _` is _left-composable_ and can implement 494 | `def $ [G[_]](g: Monad[G]): Monad[R ⇒ G[_]]`. 495 | 496 | So, for monads `f` and `g` their composition `f $ g` will be a monad either 497 | when `f` is left-composable or `g` is right-composable or visa versa for `&`. 498 | When in the expression `f $ g` `f` is left-composable and `g` is right-composable, 499 | `f`'s `$` method will be used to construct a composition. When in the expression 500 | `g & f` `f` is left-composable and `g` is right-composable, `g`'s `&` method will 501 | be used to construct a composition. 502 | 503 | The same holds for semi-monads. 504 | 505 | Check [`instances.scala`](https://github.com/aztek/scala-workflow/blob/master/src/main/scala/scala/workflow/instances.scala) 506 | to ensure, which monads are left or right composable and also [composition specs](https://github.com/aztek/scala-workflow/blob/master/src/test/scala/scala/workflow/CompositionSpec.scala) 507 | for exact rules of composition of different workflow classes. 508 | 509 | Methods `$` and `&` are called _monad transformer_ elsewhere, although 510 | separation of left and right composability is usually not introduced. 511 | 512 | Examples 513 | -------- 514 | ### Evaluator for a language of expressions 515 | [Original paper](http://strictlypositive.org/IdiomLite.pdf) by McBride and 516 | Patterson, that introduced idiom brackets, describes a simple evaluator for a 517 | language of expressions. Following that example, here's how it would look like 518 | here. 519 | 520 | We start with the definition of abstract syntax tree of a language with 521 | integers, variables and a plus. 522 | 523 | ```scala 524 | sealed trait Expr 525 | case class Var(id: String) extends Expr 526 | case class Val(value: Int) extends Expr 527 | case class Add(lhs: Expr, rhs: Expr) extends Expr 528 | ``` 529 | 530 | Variables are fetched from the environment of type `Env`. 531 | 532 | ```scala 533 | type Env = Map[String, Int] 534 | def fetch(x: String)(env: Env): Option[Int] = env.get(x) 535 | ``` 536 | 537 | The evaluator itself is a function of type `Expr ⇒ Env ⇒ Option[Int]`. 538 | 539 | ```scala 540 | def eval(expr: Expr)(env: Env): Option[Int] = 541 | expr match { 542 | case Var(x) ⇒ fetch(x)(env) 543 | case Val(value) ⇒ Some(value) 544 | case Add(x, y) ⇒ for { 545 | lhs ← eval(x)(env) 546 | rhs ← eval(y)(env) 547 | } yield lhs + rhs 548 | } 549 | ``` 550 | 551 | Note, that one have to explicitly pass the environment around and to have 552 | rather clumsy syntax to compute the addition. This can be simplified, once 553 | wrapped into the workflow `Env ⇒ Option[_]`, which can either be constructed 554 | by hand, or composed of `Env ⇒ _` and `Option` workflows. 555 | 556 | ```scala 557 | def eval: Expr ⇒ Env ⇒ Option[Int] = 558 | context(function[Env] $ option) { 559 | case Var(x) ⇒ fetch(x) 560 | case Val(value) ⇒ $(value) 561 | case Add(x, y) ⇒ $(eval(x) + eval(y)) 562 | } 563 | ``` 564 | 565 | ### Functional reactive programming 566 | `scala-workflow` can be used as syntactic sugar for external libraries and 567 | programming paradigms. In this example, a very simple version of [Functional 568 | reactive programming](http://en.wikipedia.org/wiki/Functional_reactive_programming) 569 | framework is implemented as `Idiom` instance. 570 | 571 | `Cell` trait defines a unit of data, that can be assigned with `:=` and fetched 572 | with `!`. Cells can depend on each others values, much like they do in 573 | spreadsheets. 574 | 575 | ```scala 576 | trait Cell[T] { 577 | def ! : T 578 | def := (value: T) { throw new UnsupportedOperationException } 579 | } 580 | ``` 581 | 582 | Workflow instance, implemented as `Idiom`, defines cells, that either contain 583 | atomic value that can be reassign or dependent cells, that take value of some 584 | other cell to compute their own (reassigning them doesn't make sense, hence 585 | the exception). 586 | 587 | ```scala 588 | val frp = new Idiom[Cell] { 589 | def point[A](a: ⇒ A) = new Cell[A] { 590 | private var value = a 591 | override def := (a: A) { value = a } 592 | def ! = value 593 | } 594 | def app[A, B](f: Cell[A ⇒ B]) = a ⇒ new Cell[B] { 595 | def ! = f!(a!) 596 | } 597 | } 598 | ``` 599 | 600 | With that instance we can organize reactive computations with simple syntax. 601 | 602 | ```scala 603 | context(frp) { 604 | val a = $(10) 605 | val b = $(5) 606 | 607 | val c = $(a + b * 2) 608 | 609 | (c!) should equal (20) 610 | 611 | b := 7 612 | 613 | (c!) should equal (24) 614 | } 615 | ``` 616 | 617 | ### Monadic interpreter for stack programming language 618 | If you enjoy embedding monadic domain-specific languages in your Scala programs, 619 | you might like syntactical perks `scala-workflow` could offer. Consider a 620 | little embedded stack programming language. 621 | 622 | We represent stack as a regular `List`, and the result of a program, that 623 | manipulates with a stack, as either a modified stack or an error message 624 | (such as "stack underflow"). 625 | 626 | ```scala 627 | type Stack = List[Int] 628 | type State = Either[String, Stack] 629 | ``` 630 | 631 | The evaluation of the program will use `state` monad, that will disregard 632 | the result of any command, but preserve the state of the stack. 633 | 634 | ```scala 635 | val stackLang = state[State] 636 | ``` 637 | 638 | Unlike State monad in Haskell or `scalaz`, `state` doesn't use any specific 639 | wrappers to represent the result of the computation, but rather works with bare 640 | functions. 641 | 642 | We would like to define stack operators as `Stack ⇒ State` functions. To be 643 | able to use them within some `workflow`, we need to lift them into the monad. 644 | In this example we are only interested in the modified state of the computation, 645 | so the result is always set to `()`. 646 | 647 | ```scala 648 | def command(f: Stack ⇒ State) = (st: State) ⇒ ((), either[String].bind(f)(st)) 649 | ``` 650 | 651 | With `command` helper we can now define a bunch of commands, working with stack. 652 | 653 | ```scala 654 | def put(value: Int) = command { 655 | case stack ⇒ Right(value :: stack) 656 | } 657 | 658 | def dup = command { 659 | case a :: stack ⇒ Right(a :: a :: stack) 660 | case _ ⇒ Left("Stack underflow while executing `dup`") 661 | } 662 | 663 | def rot = command { 664 | case a :: b :: stack ⇒ Right(b :: a :: stack) 665 | case _ ⇒ Left("Stack underflow while executing `rot`") 666 | } 667 | 668 | def sub = command { 669 | case a :: b :: stack ⇒ Right((b - a) :: stack) 670 | case _ ⇒ Left("Stack underflow while executing `sub`") 671 | } 672 | ``` 673 | 674 | Execution of the program on the empty stack simply takes the modified stack. 675 | 676 | ```scala 677 | def execute(program: State ⇒ (Unit, State)) = { 678 | val (_, state) = program(Right(Nil)) 679 | state 680 | } 681 | ``` 682 | 683 | Now, working inside `stackLang` context we can write programs as sequences of 684 | stack commands and execute them to get modified state of the stack. 685 | 686 | ```scala 687 | context(stackLang) { 688 | val programA = $ { put(5); dup; put(7); rot; sub } 689 | execute(programA) should equal(Right(List(2, 5))) 690 | 691 | val programB = $ { put(5); dup; sub; rot; dup } 692 | execute(programB) should equal(Left("Stack underflow while executing `rot`")) 693 | } 694 | ``` 695 | 696 | ### Point-free notation 697 | If you're familiar with [SKI-calculus](http://en.wikipedia.org/wiki/SKI_combinator_calculus), 698 | you might notice, that `point` and `app` methods of `function[R]` workflow 699 | are in fact `K` and `S` combinators. This means that you can construct any 700 | closed lambda-term (in other words, any function) with just those two methods. 701 | 702 | For instance, here's how you can define `I`-combinator (the identity function): 703 | 704 | ```scala 705 | // I = S K K 706 | def id[T] = function[T].app(function[T ⇒ T].point)(function[T].point) 707 | ``` 708 | 709 | Or `B`-combinator (Scalas `Function.compose` method or Haskells `(.)`): 710 | 711 | ```scala 712 | // B = S (K S) K 713 | def b[A, B, C] = function[A ⇒ B].app(function[A ⇒ B].point(function[C].app[A, B]))(function[C].point) 714 | // More concisely with map 715 | def b[A, B, C] = function[A ⇒ B].map(function[C].app[A, B])(function[C].point) 716 | ``` 717 | 718 | Aside from mind-boggling examples like that, there's actually a useful 719 | application for these workflows — they can be used in point-free notation, 720 | i.e. for construction of complex functions with no function arguments 721 | specified. 722 | 723 | Point-free notation support is rather limited in Scala, compared to Haskell, 724 | but some things still could be done. Here are some examples to get you inspired. 725 | 726 | ```scala 727 | context(function[Char]) { 728 | val isLetter: Char ⇒ Boolean = _.isLetter 729 | val isDigit: Char ⇒ Boolean = _.isDigit 730 | 731 | // Traditionally 732 | val isLetterOrDigit = (ch: Char) ⇒ isLetter(ch) || isDigit(ch) 733 | 734 | // Combinatorially 735 | val isLetterOrDigit = $(isLetter || isDigit) 736 | } 737 | ``` 738 | 739 | Scalas `Function.compose` and `Function.andThen` also come in handy. 740 | 741 | ```scala 742 | context(function[Double]) { 743 | val sqrt: Double ⇒ Double = x ⇒ math.sqrt(x) 744 | val sqr: Double ⇒ Double = x ⇒ x * x 745 | val log: Double ⇒ Double = x ⇒ math.log(x) 746 | 747 | // Traditionally 748 | val f = (x: Double) ⇒ sqrt((sqr(x) - 1) / (sqr(x) + 1)) 749 | 750 | // Combinatorially 751 | val f = sqrt compose $((sqr - 1) / (sqr + 1)) 752 | 753 | // Traditionally 754 | val g = (x: Double) ⇒ (sqr(log(x)) - 1) / (sqr(log(x)) + 1) 755 | 756 | // Combinatorially 757 | val g = log andThen $((sqr - 1) / (sqr + 1)) 758 | } 759 | ``` 760 | 761 | ### Purely functional logging 762 | It is no secret for a functional programmer that monads are extremely powerful. 763 | In fact, most of the features of imperative programming languages, that are 764 | usually implemented with variations of mutable state and uncontrolled side 765 | effects can be expressed with monads. However, functional purity often comes 766 | with the price of rather cumbersome syntax, compared to equivalent imperative 767 | constructs. 768 | 769 | `scala-workflow` among other things tries to bridge this gap. In this example 770 | it is used to allow snippets of imperative-looking code to mix pure computations 771 | and logging in purely functional manner. 772 | 773 | Workflow instance for this example will be `accumulator`. 774 | 775 | ```scala 776 | val logging = accumulator[List[String]] 777 | ``` 778 | 779 | Accumulator monad (called `Writer` monad elsewhere) captures a computation that 780 | aside from producing the result, accumulates some auxiliary output. In this case 781 | it's message in a log, represented as plain list of strings. In general case, 782 | type with defined [`Monoid`](https://github.com/aztek/scala-workflow/blob/master/src/main/scala/scala/workflow/auxiliary.scala) instance is expected. 783 | 784 | Regular functions naturally don't produce any log messages in `accumulator` 785 | monad sense. To produce log message, a function must return a tuple of result 786 | of the computation and a list. 787 | 788 | ```scala 789 | def mult(x: Int, y: Int) = (x * y, List(s"Calculating $x * $y")) 790 | ``` 791 | 792 | Functions, that produce no result and some log messages should return a tuple 793 | with unit result. 794 | 795 | ```scala 796 | def info(message: String) = (Unit, List(message)) 797 | ``` 798 | 799 | Having a snippet of code wrapped in `workflow(logging)`, we can intersperse 800 | pure computations with writing to a log, much like we would do with `log4j` or 801 | similar framework. 802 | 803 | ```scala 804 | val (result, log) = workflow(logging) { 805 | info("Lets define a variable") 806 | val x = 2 807 | 808 | info("And calculate a square of it") 809 | val square = mult(x, x) 810 | 811 | info("Also a cube and add them together") 812 | val cube = mult(mult(x, x), x) 813 | val sum = square + cube 814 | 815 | info("This is all so silly") 816 | sum / 2 817 | } 818 | ``` 819 | 820 | The result of computation is now stored in `result` and the log is in 821 | `log`. Note, that no side effects or mutable state whatsoever were 822 | involved in this example. 823 | 824 | ```scala 825 | result should equal (6) 826 | log should equal (List("Lets define a variable", 827 | "And calculate a square of it", 828 | "Calculating 2 * 2", 829 | "Also a cube and add them together", 830 | "Calculating 2 * 2", 831 | "Calculating 4 * 2", 832 | "This is all so silly")) 833 | ``` 834 | 835 | Disclaimer 836 | ---------- 837 | This project is very experimental and your comments and suggestions are highly 838 | appreciated. Drop me a line [on twitter](http://twitter.com/aztek) or 839 | [by email](mailto:evgeny.kotelnikov@gmail.com), or [open an issue](./issues/new) 840 | here on GitHub. I'm also occasionally on #scala IRC channel on Freenode. 841 | --------------------------------------------------------------------------------