├── 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 [](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 | Inside the $ |
252 | Generated code |
253 | Pure Scala counterpart |
254 |
255 |
256 | $(42) |
257 | option.point(42) |
258 | Some(42)
259 | |
260 |
261 | $(Some(42) + 1) |
262 | option.map(
263 | (x$1: Int) ⇒
264 | x$1 + 1
265 | )(Some(42)) |
266 | for {
267 | x ← Some(42)
268 | } yield x + 1
269 | |
270 |
271 |
272 | $(Some(2) * Some(3)) |
273 | option.app(
274 | option.map(
275 | (x$1: Int) ⇒
276 | (x$2: Int) ⇒
277 | x$1 * x$2
278 | )(Some(2))
279 | )(Some(3)) |
280 | for {
281 | x ← Some(2)
282 | y ← Some(3)
283 | } yield x * y
284 | |
285 |
286 |
287 | $(divide(1, 2)) |
288 | divide(1, 2) |
289 | divide(1, 2) |
290 |
291 |
292 | $(divide(Some(1.5), 2)) |
293 | option.bind(
294 | (x$1: Double) ⇒
295 | divide(x$1, 2)
296 | )(Some(1.5)) |
297 | for {
298 | x ← Some(1.5)
299 | y ← divide(x, 2)
300 | } yield y
301 | |
302 |
303 |
304 | $(divide(Some(1.5), Some(2))) |
305 | 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)) |
312 | for {
313 | x ← Some(1.5)
314 | y ← Some(2)
315 | z ← divide(x, y)
316 | } yield z
317 | |
318 |
319 |
320 | $(divide(Some(1.5), 2) + 1) |
321 | 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)) |
328 | for {
329 | x ← Some(1.5)
330 | y ← divide(x, 2)
331 | } yield y + 1
332 | |
333 |
334 |
335 |
336 | #### Blocks of code
337 |
338 |
339 | Inside the $ |
340 | Generated code |
341 | Pure Scala counterpart |
342 |
343 |
344 | $ {
345 | val x = Some(10)
346 | x + 2
347 | } |
348 | option.map(
349 | (x$1: Int) ⇒
350 | x$1 + 2
351 | )(Some(10)) |
352 | for {
353 | x ← Some(10)
354 | } yield x + 2 |
355 |
356 |
357 | $ {
358 | val x = Some(10)
359 | val y = Some(5)
360 | x + y
361 | } |
362 | option.bind(
363 | (x$1: Int) ⇒
364 | option.map(
365 | (x$2: Int) ⇒
366 | x$1 + x$2
367 | )(Some(5))
368 | )(Some(10)) |
369 | for {
370 | x ← Some(10)
371 | y ← Some(5)
372 | } yield x + y |
373 |
374 |
375 | $ {
376 | val x = Some(10)
377 | val y = x − 3
378 | x * y
379 | } |
380 | option.map(
381 | (x$1: Int) ⇒
382 | val y = x$1 − 3
383 | x$1 * y
384 | )(Some(10)) |
385 | for {
386 | x ← Some(10)
387 | y = x - 3
388 | } yield x * y |
389 |
390 |
391 | $(2 + {
392 | val x = Some(10)
393 | x * 2
394 | }) |
395 | option.map(
396 | (x$1: Int) ⇒
397 | 2 + x$1
398 | )(option.map(
399 | (x$2: Int) ⇒
400 | x$2 * 2
401 | )(Some(10))) |
402 | for {
403 | y ← for {
404 | x ← Some(10)
405 | } yield x * 2
406 | } yield 2 + y |
407 |
408 |
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 |
--------------------------------------------------------------------------------