├── .gitignore ├── project ├── plugins.sbt └── build.properties ├── shippable.yml ├── src ├── test │ └── scala │ │ ├── Bugs.scala │ │ ├── Utils.scala │ │ ├── NonCompilation.scala │ │ ├── Assumptions.scala │ │ ├── ExpressionSpec.scala │ │ ├── Examples.scala │ │ ├── DoNotationSpec.scala │ │ ├── IdiomBracketSpec.scala │ │ └── CodeGeneration.scala └── main │ └── scala │ └── Expression.scala └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | project/target/ -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.8 -------------------------------------------------------------------------------- /shippable.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.6 4 | jdk: 5 | - oraclejdk8 6 | -------------------------------------------------------------------------------- /src/test/scala/Bugs.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import org.specs2.ScalaCheck 4 | import org.specs2.mutable._ 5 | import Expression.extract 6 | 7 | import scalaz.std.option._ 8 | 9 | import scalaz.Apply 10 | 11 | class Bugs extends Specification with ScalaCheck { 12 | 13 | "bugs" should { 14 | "canBuildFrom" ! prop { (a: Option[List[String]], b: String => String) => 15 | val f = Expression[Option, List[String]](extract(a).map(b)) 16 | f ==== Apply[Option].map(a)(_.map(b)) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/scala/Utils.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | package object utils { 4 | trait Range 5 | case class Last(i: Int) extends Range 6 | case class Take(i: Int) extends Range 7 | case class Drop(i: Int) extends Range 8 | case class Slice(from: Int, to: Int) extends Range 9 | implicit class AugmentedSeq[A](seq: Seq[A]) { 10 | def slice(range: Range): Seq[A] = { 11 | range match { 12 | case Take(i) => seq.take(i) 13 | case Drop(i: Int) => seq.drop(i) 14 | case Last(i) => seq.takeRight(i) 15 | case Slice(from, to) => seq.slice(from, to) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/scala/NonCompilation.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import org.specs2.mutable._ 4 | 5 | import scala.tools.reflect.ToolBoxError 6 | import scalaz.{Monad, Applicative} 7 | 8 | import scala.reflect.runtime.universe._ 9 | import scala.reflect.runtime.{currentMirror => cm} 10 | import scala.tools.reflect.ToolBox 11 | import scala.tools.reflect.ToolBoxError 12 | 13 | class NonCompilation extends Specification { 14 | 15 | "compile errors" should { 16 | "extract does not compile on it's own" in { 17 | val ast = q""" 18 | import com.github.jedesah.Expression.extract 19 | val a: Option[String] = ??? 20 | def doThing(a: String): String = ??? 21 | doThing(extract(a)) 22 | """ 23 | val tb = cm.mkToolBox() 24 | tb.compile(ast) must throwA[ToolBoxError]("`extract` must be enclosed in an `Expression`") 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/scala/Assumptions.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.specs2.mutable._ 6 | 7 | import scala.concurrent.Await 8 | import scalaz.Monad 9 | 10 | class Assumptions extends Specification { 11 | 12 | "assumptions" should { 13 | "bind over Future does not require futures that are part of the continuation to complete in order to complete" in { 14 | import scala.concurrent.Future 15 | import scala.concurrent.Promise 16 | import scala.concurrent.ExecutionContext.Implicits.global 17 | import scala.concurrent.duration._ 18 | 19 | val aPromise = Promise[Boolean]() 20 | val a = aPromise.future 21 | 22 | val bPromise = Promise[String]() 23 | val b = bPromise.future 24 | 25 | val cPromise = Promise[String]() 26 | val c = cPromise.future 27 | 28 | implicit val monad: Monad[Future] = scalaz.std.scalaFuture.futureInstance 29 | 30 | val f = Monad[Future].bind(a)(if(_) b else c) 31 | 32 | f.value ==== None 33 | 34 | aPromise.success(true) 35 | 36 | f.value ==== None 37 | 38 | bPromise.success("hello") 39 | 40 | Await.result(f, FiniteDuration(1, TimeUnit.SECONDS)) ==== "hello" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/scala/ExpressionSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import com.github.jedesah.Expression._ 6 | import org.scalacheck.Arbitrary 7 | import org.scalacheck.Arbitrary._ 8 | import org.specs2.ScalaCheck 9 | import org.specs2.mutable._ 10 | import shapeless._ 11 | 12 | import scala.concurrent.Await 13 | import scalaz.{Applicative, Apply, Monad} 14 | 15 | class ExpressionSpec extends Specification with ScalaCheck { 16 | 17 | implicit def FutureArbitrary[A: Arbitrary]: Arbitrary[scala.concurrent.Future[A]] = 18 | Arbitrary(arbitrary[A] map ((x: A) => scala.concurrent.Future.successful(x))) 19 | 20 | type ThirteenOptions[A] = Option[A] :: 21 | Option[A] :: 22 | Option[A] :: 23 | Option[A] :: 24 | Option[A] :: 25 | Option[A] :: 26 | Option[A] :: 27 | Option[A] :: 28 | Option[A] :: 29 | Option[A] :: 30 | Option[A] :: 31 | Option[A] :: 32 | Option[A] :: HNil 33 | 34 | "Expression" should { 35 | "support Monad when there is a Monad instance available" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], doThing: (String, String, String) => String, firstThis: String => Option[String]) => 36 | import scalaz.std.option.optionInstance 37 | val f = Expression[Option, String](doThing(extract(firstThis(extract(a))), extract(b), extract(c))) 38 | f ==== Applicative[Option].apply3(Monad[Option].bind(a)(firstThis), b, c)(doThing) 39 | } 40 | "only require Applicative when the Monad typeclass is not required" ! prop { (a: Option[String], b: Option[String], doThing: (String, String) => String) => 41 | implicit val instance: Applicative[Option] = scalaz.std.option.optionInstance 42 | val f = Expression[Option, String](doThing(extract[Option, String](a), extract[Option, String](b))) 43 | f ==== Apply[Option].apply2(a, b)(doThing) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/Examples.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import java.util.concurrent.{TimeoutException, TimeUnit} 4 | 5 | import Expression.extract 6 | import org.scalacheck.Arbitrary 7 | import org.scalacheck.Arbitrary._ 8 | import org.specs2.ScalaCheck 9 | import org.specs2.mutable._ 10 | 11 | import scala.concurrent.duration._ 12 | import scala.concurrent.{Await, Future} 13 | import scala.util.Try 14 | import scalaz.std.scalaFuture.futureInstance 15 | import scalaz._ 16 | import scalaz.syntax.writer._ 17 | import scalaz.std.string._ 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | 21 | class Examples extends Specification with ScalaCheck { 22 | 23 | sequential 24 | 25 | implicit def FutureArbitrary[A: Arbitrary]: Arbitrary[scala.concurrent.Future[A]] = 26 | Arbitrary(arbitrary[A] map ((x: A) => scala.concurrent.Future.successful(x))) 27 | 28 | implicit val phoneArbitrary: Arbitrary[Phone] = 29 | Arbitrary(arbitrary[String] map ((x: String) => Phone(x))) 30 | 31 | implicit val addressArbitrary: Arbitrary[Address] = 32 | Arbitrary(arbitrary[String] map ((x: String) => Address(x))) 33 | 34 | case class Phone(repr: String) 35 | case class Address(repr: String) 36 | type Score = Int 37 | type HTML = String 38 | 39 | "Examples" >> { 40 | "features" >> { 41 | "flexible use of abstractions" in { 42 | val combine = (a: Int, b: Int, c: Int) => a + b + c 43 | "the for comprehension version does not fail-fast" in { 44 | val a: Future[Int] = Future{Thread.sleep(1000); 10} 45 | val b: Future[Int] = Future.successful(2) 46 | val c: Future[Int] = Future{Thread.sleep(10); throw new Exception} 47 | val result: Future[Int] = for (aa <- a; bb <- b; cc <- c) yield combine(aa,bb,cc) 48 | Await.ready(result, 500.milliseconds) should throwA[TimeoutException] 49 | } 50 | "the scalaz version does fail-fast" in { 51 | val a: Future[Int] = Future{Thread.sleep(1000); 10} 52 | val b: Future[Int] = Future.successful(2) 53 | val c: Future[Int] = Future{Thread.sleep(10); throw new Exception} 54 | val result = Applicative[Future].apply3(a,b,c)(combine) 55 | // This will throw an exception if we are stuck waiting for the first Future 56 | Await.ready(result, 500.milliseconds) 57 | ok( """Future terminated before timeout which indicates "failing fast"""") 58 | } 59 | "the Expression version does fail-fast" in { 60 | import Expression.auto.extract 61 | val a: Future[Int] = Future{Thread.sleep(1000); 10} 62 | val b: Future[Int] = Future.successful(2) 63 | val c: Future[Int] = Future{Thread.sleep(10); throw new Exception} 64 | val result: Future[Int] = Expression[Future, Int]{ combine(a, b, c) } 65 | // This will throw an exception if we are stuck waiting for the first Future 66 | Await.ready(result, 500.milliseconds) 67 | ok( """Future terminated before timeout which indicates "failing fast"""") 68 | } 69 | } 70 | "Playing well with if statements and match statements" in { 71 | "the naive for comprehension version is not smart about which Future's it actually needs" in { 72 | val a: Future[String] = Future.successful("Hello") 73 | val b: Future[Int] = Future{Thread.sleep(1000); 10} 74 | val c: Future[Int] = Future{Thread.sleep(10); 8} 75 | val polish = (a: Int) => a * 2 76 | val result: Future[Int] = for { 77 | aa <- a 78 | bb <- b 79 | cc <- c 80 | } yield if (aa == "something") polish(bb) else polish(cc) 81 | Await.ready(result, 500.milliseconds) should throwA[TimeoutException] 82 | } 83 | "the ceremonious version works but does not afford much over using the abstraction directly" in { 84 | val a: Future[String] = Future.successful("Hello") 85 | val b: Future[Int] = Future{Thread.sleep(1000); 10} 86 | val c: Future[Int] = Future{Thread.sleep(10); 8} 87 | val polish = (a: Int) => a * 2 88 | val result: Future[Int] = (for (aa <- a) yield 89 | if (aa == "something") for (bb <- b) yield polish(bb) 90 | else for (cc <- c) yield polish(cc)).flatMap(identity) 91 | Await.ready(result, 500.milliseconds) 92 | ok("Future terminated before timeout which means we did not wait on b") 93 | } 94 | "the Expression version works without ceremony" in { 95 | import Expression.auto.extract 96 | val a: Future[String] = Future.successful("Hello") 97 | val b: Future[Int] = Future{Thread.sleep(1000); 10} 98 | val c: Future[Int] = Future{Thread.sleep(10); 8} 99 | val polish = (a: Int) => a * 2 100 | val result: Future[Int] = Expression[Future,Int](if (extract(a) == "something") polish(b) else polish(c)) 101 | Await.ready(result, 500.milliseconds) 102 | ok("Future terminated before timeout which means we did not wait on b") 103 | } 104 | } 105 | "Manipulating Context" in { 106 | "Writer" in { 107 | "for-comprehension version works but is less elegant" in { 108 | def random() = 0 109 | val aString = "This is a magic value" 110 | val bString = "I got this value from a dirty function" 111 | val cString = "Hello World..." 112 | val ctxString = "We avoided a division by zero, yay!" 113 | val a: Writer[String, Int] = 8.set(aString) 114 | val b: Writer[String, Int] = random().set(bString) 115 | val c: Writer[String, String] = "Hello World".set(cString) 116 | val result = for { 117 | aa <- a 118 | bb <- b 119 | div <- if (bb == 0) { 120 | 5.set(ctxString) 121 | } else Writer("", aa / bb) 122 | cc <- c 123 | } yield cc * div 124 | result ==== Writer(aString + bString + ctxString + cString, "Hello World" * 5) 125 | } 126 | "Expression version" in { 127 | pending 128 | } 129 | } 130 | "Future" in { 131 | "for-comprehension version" in { 132 | val a: Future[Int] = Future.successful(4) 133 | val b: Future[Int] = Future.successful(0) 134 | val c: Future[String] = Future.successful("Hello World") 135 | val result = for { 136 | aa <- a 137 | bb <- b 138 | div <- if (bb == 0) { 139 | Future.failed(new Exception("Division by zero :-(")) 140 | } else Future.successful(aa / bb) 141 | cc <- c 142 | } yield cc * div; 143 | // Await.result(result, 1.second) should throwAn[Exception] does not work for some reason... 144 | Try(Await.result(result, 1.second)).isFailure ==== true 145 | } 146 | "Expression version" in { 147 | pending 148 | } 149 | } 150 | } 151 | } 152 | "Usage" in { 153 | "explicit" ! prop { (phoneString: String, 154 | lookupPhone: String => Future[Phone], 155 | lookupAddress: Phone => Future[Address], 156 | lookupReputation: Phone => Future[Score], 157 | renderPage: (Phone, Address, Int) => HTML) => 158 | 159 | val f: Future[HTML] = Expression.monad[Future, HTML] { 160 | val phone = lookupPhone(phoneString) 161 | val address = lookupAddress(extract(phone)) 162 | val rep = lookupReputation(extract(phone)) 163 | renderPage(extract(phone), extract(address), extract(rep)) 164 | } 165 | val expected = { 166 | val phone = lookupPhone(phoneString) 167 | val address = Monad[Future].bind(phone)(lookupAddress) 168 | val rep = Monad[Future].bind(phone)(lookupReputation) 169 | Applicative[Future].apply3(phone, address, rep)(renderPage) 170 | } 171 | val timeout = FiniteDuration(100, TimeUnit.MILLISECONDS) 172 | Await.result(f, timeout) ==== Await.result(expected, timeout) 173 | } 174 | "implicit" ! prop { (phoneString: String, 175 | lookupPhone: String => Future[Phone], 176 | lookupAddress: Phone => Future[Address], 177 | lookupReputation: Phone => Future[Score], 178 | renderPage: (Phone, Address, Int) => HTML) => 179 | import com.github.jedesah.Expression.auto.extract 180 | val f: Future[HTML] = Expression.monad[Future, HTML] { 181 | val phone = lookupPhone(phoneString) 182 | val address = lookupAddress(phone) 183 | val rep = lookupReputation(phone) 184 | renderPage(phone, address, rep) 185 | } 186 | val expected = { 187 | val phone = lookupPhone(phoneString) 188 | val address = Monad[Future].bind(phone)(lookupAddress) 189 | val rep = Monad[Future].bind(phone)(lookupReputation) 190 | Applicative[Future].apply3(phone, address, rep)(renderPage) 191 | } 192 | val timeout = FiniteDuration(100, TimeUnit.MILLISECONDS) 193 | Await.result(f, timeout) ==== Await.result(expected, timeout) 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/test/scala/DoNotationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.scalacheck.Arbitrary 6 | import org.scalacheck.Arbitrary._ 7 | import org.specs2.ScalaCheck 8 | import org.specs2.mutable._ 9 | import Expression.{extract, bind2, bind3} 10 | import shapeless._ 11 | 12 | import scala.concurrent.Await 13 | import scalaz.{Monad, Applicative, Apply} 14 | import scalaz.std.option._ 15 | import scalaz.std.list._ 16 | import shapeless.contrib.scalaz._ 17 | 18 | class DoNotationSpec extends Specification with ScalaCheck { 19 | 20 | implicit def FutureArbitrary[A: Arbitrary]: Arbitrary[scala.concurrent.Future[A]] = 21 | Arbitrary(arbitrary[A] map ((x: A) => scala.concurrent.Future.successful(x))) 22 | 23 | type ThirteenOptions[A] = Option[A] :: 24 | Option[A] :: 25 | Option[A] :: 26 | Option[A] :: 27 | Option[A] :: 28 | Option[A] :: 29 | Option[A] :: 30 | Option[A] :: 31 | Option[A] :: 32 | Option[A] :: 33 | Option[A] :: 34 | Option[A] :: 35 | Option[A] :: HNil 36 | 37 | "Expression.monad" should { 38 | "double nested extract within argument" in { 39 | "simple enough" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], doThing: (String, String, String) => String, firstThis: String => Option[String]) => 40 | val f = Expression.monad[Option, String](doThing(extract(firstThis(extract(a))), extract(b), extract(c))) 41 | f ==== Applicative[Option].apply3(Monad[Option].bind(a)(firstThis), b, c)(doThing) 42 | } 43 | "nested a little deeper" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], doThing: (String, String, String) => String, firstThis: String => Option[String], other: String => String) => 44 | val f = Expression.monad[Option, String](doThing(other(extract(firstThis(extract(a)))), extract(b), extract(c))) 45 | f ==== Applicative[Option].apply3(Applicative[Option].map(Monad[Option].bind(a)(firstThis))(other), b, c)(doThing) 46 | } 47 | "with 2 monads inside first extract" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], 48 | doThing: (String, String) => String, 49 | firstThis: (String, String) => Option[String], 50 | other: String => String) => 51 | val f = Expression.monad[Option, String](doThing(other(extract(firstThis(extract(a), extract(b)))), extract(c))) 52 | f == Applicative[Option].apply2( 53 | Applicative[Option].map(bind2(a, b)(firstThis))(other), c 54 | )(doThing) 55 | } 56 | "tricky function that takes a monad and extracts itself. Want to make sure we are not to eager to lift things" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], doThing: (String, String, String) => String, firstThis: String => String, other: String => String) => 57 | val f = Expression.monad[Option, String](doThing(other(firstThis(extract(a))), extract(b), extract(c))) 58 | f ==== Applicative[Option].apply3(Applicative[Option].map(Applicative[Option].map(a)(firstThis))(other), b, c)(doThing) 59 | } 60 | "if/else expression that is like a monadic function" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[Int], doThing: (String, String, String) => String, other: String => String) => 61 | val f = Expression.monad[Option, String] { 62 | doThing(other(extract(if (extract(a) == "") Some("a") else None)), extract(b), extract(c)) 63 | } 64 | f ==== Applicative[Option].apply3(Applicative[Option].map(Monad[Option].bind(a)(f => if (f == "") Some("a") else None))(other), b, c)(doThing) 65 | } 66 | "interpolated String" ! prop { (a: Option[String]) => 67 | val f = Expression.monad[Option, String] { 68 | s"It is ${extract(a)}!" 69 | } 70 | f ==== Applicative[Option].map(a)(aa => s"It is $aa!") 71 | } 72 | } 73 | "is lazy with if/else" in { 74 | import scala.concurrent.Future 75 | import scala.concurrent.Promise 76 | import scala.concurrent.ExecutionContext.Implicits.global 77 | import scala.concurrent.duration._ 78 | 79 | val aPromise = Promise[Boolean]() 80 | val a = aPromise.future 81 | 82 | val bPromise = Promise[String]() 83 | val b = bPromise.future 84 | 85 | val cPromise = Promise[String]() 86 | val c = cPromise.future 87 | 88 | implicit val monad: Monad[Future] = scalaz.std.scalaFuture.futureInstance 89 | 90 | val f = Expression.monad[Future, String](if (extract(a)) extract(b) else extract(c)) 91 | 92 | f.value ==== None 93 | 94 | aPromise.success(true) 95 | 96 | f.value ==== None 97 | 98 | bPromise.success("hello") 99 | 100 | Await.result(f, FiniteDuration(1, TimeUnit.SECONDS)) ==== "hello" 101 | } 102 | tag("match") 103 | "with match (iGraph example)" ! prop { (namesOption: Option[Map[String, String]], userLocale: String) => 104 | sealed trait LocationType 105 | case class City(city: Option[String]) extends LocationType 106 | case class Country(countryName: Option[String]) extends LocationType 107 | case object State extends LocationType 108 | val loc: LocationType = City(None) 109 | // 'EN-US' will be "normalized" to 'en' 110 | def normalize(locale: String) = locale.take(2).toLowerCase 111 | val f = Expression.monad[Option, LocationType] { 112 | val names = extract(namesOption) 113 | val normalizedMap = names.map { case (key, value) => (normalize(key), value)} 114 | val name = extract(normalizedMap.get(normalize(userLocale)).orElse(normalizedMap.get("en"))) 115 | // If there is no user requested locale or english, leave Location unchanged 116 | loc match { 117 | case loc:City => loc.copy(city = Some(name)) 118 | case loc:Country => loc.copy(countryName = Some(name)) 119 | case _ => loc 120 | } 121 | } 122 | val expected = { 123 | val names = namesOption 124 | val normalizedMap = Applicative[Option].map(names)(x1 => x1.map { case (key, value) => (normalize(key), value)}) 125 | val name = Monad[Option].bind(normalizedMap)(x1 => x1.get(normalize(userLocale)).orElse(x1.get("en"))) 126 | loc match { 127 | case loc: City => Applicative[Option].map(name)(x1 => loc.copy(city = Some(x1))) 128 | case loc: Country => Applicative[Option].map(name)(x1 => loc.copy(countryName = Some(x1))) 129 | case _ => Applicative[Option].pure(loc) 130 | } 131 | } 132 | f ==== expected 133 | } 134 | tag("block") 135 | "block" in { 136 | "1" ! prop { (a: Option[String], foo: String => Option[String], bar: String => String) => 137 | val f = Expression.monad[Option, String] { 138 | val b = foo(extract(a)) 139 | bar(extract(b)) 140 | } 141 | f ==== Monad[Option].bind(a)(aa => Applicative[Option].map(foo(aa))(bar)) 142 | } 143 | "2" ! prop { (a: Option[String], d: Option[String], foo: String => Option[String], bar: (String, String) => String, biz: String => Option[String]) => 144 | val f = Expression.monad[Option, String] { 145 | val b = foo(extract(a)) 146 | val c = biz(extract(d)) 147 | bar(extract(b), extract(c)) 148 | } 149 | f ==== { 150 | val b = Applicative[Option].map(a)(foo) 151 | val c = Applicative[Option].map(d)(biz) 152 | Applicative[Option].apply2(Monad[Option].join(b),Monad[Option].join(c))(bar) 153 | } 154 | } 155 | } 156 | tag("match") 157 | "match" in { 158 | "simple" ! prop { (a: Option[String], b: Option[String], c: Option[String], foo: String => String, bar: String => String) => 159 | val f = Expression.monad[Option, String] { 160 | extract(a) match { 161 | case "" => foo(extract(b)) 162 | case _ => bar(extract(c)) 163 | } 164 | } 165 | val expected = Monad[Option].bind(a){ 166 | case "" => Apply[Option].map(b)(foo) 167 | case _ => Apply[Option].map(c)(bar) 168 | } 169 | f ==== expected 170 | } 171 | "more complex" ! prop { (a: Option[String], b: Option[String], c: Option[String], d: Option[String], foo: String => String, bar: String => String) => 172 | val f = Expression.monad[Option, String] { 173 | val dd = extract(d) 174 | extract(a) match { 175 | case `dd` => foo(extract(b)) 176 | case _ => bar(extract(c)) 177 | } 178 | } 179 | val expected = bind2(a, d) { (aa, dd) => aa match { 180 | case `dd` => Apply[Option].map(b)(foo) 181 | case _ => Apply[Option].map(c)(bar) 182 | } 183 | } 184 | f ==== expected 185 | }.pendingUntilFixed("Not yet implemented to take advantage of Monad") 186 | "with multiple stable identifiers in the pattern matches of case statements" ! prop { 187 | (a: Option[String], 188 | b: Option[String], 189 | c: Option[String], 190 | d: Option[String], 191 | e: Option[String], 192 | f: String, 193 | foo: String => String, 194 | bar: String => String) => 195 | val result = Expression.monad[Option, String] { 196 | val dd = extract(d) 197 | val ee = extract(e) 198 | extract(a) match { 199 | case `dd` => foo(extract(b)) 200 | case `ee` => bar(extract(c)) 201 | case _ => f 202 | } 203 | } 204 | val expected = bind2(a, d) { (aa, dd) => aa match { 205 | case `dd` => Apply[Option].map(b)(foo) 206 | case _ => Apply[Option].map(c)(bar) 207 | } 208 | } 209 | result ==== expected 210 | }.pendingUntilFixed("Not yet implemented") 211 | } 212 | } 213 | /*"SIP-22 example" ! prop { (optionDOY: Option[String]) => 214 | val date = """(\d+)/(\d+)""".r 215 | case class Ok(message: String) 216 | case class NotFound(message: String) 217 | def nameOfMonth(num: Int): Option[String] = None 218 | 219 | val f = IdiomBracket.monad[Option, Any] { 220 | extract(optionDOY) match { 221 | case date(month, day) => 222 | Ok(s"It’s ${extract(nameOfMonth(month.toInt))}!") 223 | case _ => 224 | NotFound("Not a date, mate!") 225 | } 226 | } 227 | }*/ 228 | "asc reverse core site" in { 229 | "without val pattern match" ! prop { (phone: Option[String], hitCounter: Option[String], locById: Option[String]) => 230 | def test(a: String, b: String): Option[(String, String)] = Some((a, b)) 231 | def otherTest(a: String, b: String, c: String): Option[String] = Some(a) 232 | 233 | val result = Expression.monad[Option, String] { 234 | val tuple: (String, String) = extract(test(extract(phone), extract(hitCounter))) 235 | extract(otherTest(tuple._2, tuple._1, extract(locById))) 236 | } 237 | val first = bind2(phone, hitCounter)(test) 238 | val expected = bind2(first, locById)((first1, locById1) => otherTest(first1._2, first1._1, locById1)) 239 | result == expected 240 | } 241 | // I don't think this is easy to support for now cuz of issues with unapply in match statement 242 | // reminder: value pattern match is transformed into a pattern match 243 | /*"with original val pattern match" ! prop { (phone: Option[String], hitCounter: Option[String], locById: Option[String]) => 244 | def test(a: String, b: String): Option[(String, String)] = Some((a, b)) 245 | def otherTest(a: String, b: String, c: String): Option[String] = Some(a) 246 | 247 | val result = IdiomBracket.monad[Option, String] { 248 | val (dict, res) = extract(test(extract(phone), extract(hitCounter))) 249 | extract(otherTest(dict, res, extract(locById))) 250 | } 251 | val first = Monad[Option].bind2(phone, hitCounter)(test) 252 | val expected = Monad[Option].bind2(first, locById)((first1, locById1) => otherTest(first1._2, first1._1, locById1)) 253 | result == expected 254 | }*/ 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expressions 2 | 3 | Expressions are an alternative for Scala for-comprehensions. 4 | 5 | Scala for-comprehensions have three drawbacks that I have identified: 6 | 7 | - They only use `flatMap` which can be unnecessarily restrictive 8 | - They don't play very well with `if` and `match` statements 9 | - It's not possible to manipulate the context from within a for-comprehension 10 | 11 | They also have some advantages: 12 | 13 | - Greater control over how effects are sequenced 14 | 15 | You should use Expressions when you don't care about the ordering of effects, you just want to operate over values in a Context and have the Contexts be merged in a sensible way. 16 | 17 | The concepts behind Expressions are loosely related to Computation Expressions from F# which is the inspiration for the name. 18 | 19 | Background on Computation Expressions: 20 | - [http://tomasp.net/academic/papers/computation-zoo/computation-zoo.pdf](http://tomasp.net/academic/papers/computation-zoo/computation-zoo.pdf) 21 | - [MSDN doc](https://msdn.microsoft.com/en-us/library/dd233182.aspx) 22 | 23 | ## Features 24 | 25 | Note that the examples below do not explicit the type parameters of the `Expression` application. For some reason, these need to be explicit, I am still investigating why, so in reality, you need to do `Expression[Future, Int]{ combine(a,b,c) }`. 26 | 27 | 28 | ### Flexible use of abstractions 29 | 30 | For-comprehensions are based on the notion of *do-notation* you may be familiar with from Haskell. This notation allows one to work with values within some kind of "context" which can be treated as a `Monad`. A `Monad` is any abstraction that supports the two following methods `point[A](a: A): F[A]` and `bind[A](fa: F[A])(f: A => F[B]): F[B]` (also known as `flatMap`). Both *do-notation* and for-comprehensions thus rewrite the "sugared" code into applications of `point` and `bind`. Unfortunately, while `bind` is a very powerful method and can be used to rewrite any expressions, its power comes at the cost of flexibility in its implementation. `Applicative` offers a less powerful abstraction that does however give more flexibility to its implementation. What does this all mean, let's look at an example: 31 | 32 | a: Future[A] 33 | b: Future[B] 34 | c: Future[C] 35 | for {aa <- a 36 | bb <- b 37 | cc <- c} yield combine(a, b, c) 38 | 39 | Let's assume `Future[A]` takes 5 seconds but `Future[C]` fails after 1 second. Let's assume we want it to "fail-fast", that is, we want the whole thing to fail immediately if `Future[C]` fails after 1 second. This behavior is simply impossible with the above code because `bind` does not support failing fast. Please see [my talk](https://www.youtube.com/watch?v=tU4pU5vaddU#t=823) at PNWScala for a more in depth explanation of why this is. 40 | 41 | Now let's look at the same example using Expressions: 42 | 43 | a: Future[A] 44 | b: Future[B] 45 | c: Future[C] 46 | Expression { combine(a,b,c) } 47 | 48 | This code will "fail-fast" because Expressions uses `ap` to combine these `Future`s which does support failing-fast 49 | 50 | ### Playing well with if statements and match statements 51 | 52 | Let's consider the following piece of code: 53 | 54 | a: Future[A] 55 | b: Future[B] 56 | c: Future[C] 57 | for {aa <- a 58 | bb <- b 59 | cc <- c} yield if (aa == something) polish(bb) else polish(cc) 60 | 61 | The above code will wait on the result of all three `Future`'s before deciding to pick either the result from `b` or `c`. That's not ideal as far as I am concerned. I would rather if it waited on `a` and then waited on either `b` or `c` because at that point we know the result of the other one is of no importance. 62 | 63 | We can fix this like this: 64 | 65 | a: Future[A] 66 | b: Future[B] 67 | c: Future[C] 68 | (for (aa <- a) yield 69 | if (aa == something) for (bb <- b) yield polish(bb) 70 | else for (cc <- c) yield polish(cc)).flatMap(identity) 71 | 72 | This code will do the right thing when it comes to discarding unnecessary `Future`s. In my book though, this is overly ceremonious. 73 | 74 | Instead, we can do this: 75 | 76 | a: Future[A] 77 | b: Future[B] 78 | c: Future[C] 79 | Expression { if(extract(a) == something) polish(b) else polish(c) } 80 | 81 | * Here is an example of somewhere automatic extraction is not triggered due to `==` being untyped. An explicit `extract` is required. 82 | 83 | ### Manipulating Context within an Expression (TODO) 84 | 85 | Let's move away from `Future`s now because this notation supports any Context. Let's consider the `Writer` Monad. This Monad is typically used for logging. 86 | 87 | for-comprehension version: 88 | 89 | a: Writer[String, Int] = 8.set("This is a magic value") 90 | b: Writer[String, Int] = random().set("I got this value from a dirty function") 91 | c: Writer[String, String] = "Hello World".set("Hello World...") 92 | for { 93 | aa <- a 94 | bb <- b 95 | div <- if (b == 0) { 96 | 5.set("We avoided a division by zero, yay!") 97 | } else Write("", aa / bb) 98 | cc <- c 99 | } yield cc * div 100 | 101 | Now with Expressions: 102 | 103 | a: Writer[String, Int] = 8.set("This is a magic value") 104 | b: Writer[String, Int] = random().set("I got this value from a dirty function") 105 | c: Writer[String, String] = "Hello World".set("Hello World...") 106 | Expression { 107 | val div = if (b == 0) { 108 | ctx :++> "We avoided a division by zero, yay!" 109 | 5 110 | } else a / b 111 | c * div 112 | } 113 | 114 | Conversely for `Future`s: 115 | 116 | for comprehension: 117 | 118 | a: Future[Int] 119 | b: Future[Int] 120 | c: Future[String] 121 | for { 122 | aa <- a 123 | bb <- b 124 | div <- if (bb == 0) { 125 | Future.failed(new Exception("Division by zero :-(")) 126 | } else Future.now(aa / bb) 127 | cc <- c 128 | } yield cc * div 129 | 130 | Now with Expressions: 131 | 132 | a: Future[Int] 133 | b: Future[Int] 134 | c: Future[String] 135 | Expression { 136 | val div = if (b == 0) { 137 | // return is a special function on the ctx that allows to change the whole Context to the value passed to it 138 | ctx.return(Future.failed(new Exception("Division by zero :-("))) 139 | } else a / b 140 | c * div 141 | } 142 | 143 | ### An example where it is better to use a for-comprehension 144 | 145 | Here is a snippert of code from [Remotely](https://github.com/oncue/remotely) 146 | 147 | private def time[A](task: Task[A]): Task[(Duration, A)] = for { 148 | t1 <- Task.delay(System.currentTimeMillis) 149 | a <- task 150 | t2 <- Task.delay(System.currentTimeMillis) 151 | } yield ((t2 - t1).milliseconds, a) 152 | 153 | If Expressions were to be used on this piece of code. The time at which System.currentTimeMillis is executed for the second time is likely to happen before the `task` is completely and thus would produce incorrect behavior. 154 | 155 | ## Getting Started 156 | 157 | ### Installation 158 | 159 | libraryDependencies += "com.github.jedesah" %% "expressions" % NOT-RELEASED-YET 160 | 161 | ### Usage 162 | 163 | Now, you can `import com.github.jedesah.Expression._` and use it like so: 164 | 165 | phoneString: String 166 | lookupPhone: String => Future[Phone] 167 | lookupAddress: Phone => Future[Address] 168 | lookupReputation: Phone => Future[Score] 169 | renderPage: (Phone, Address, Int) => HTML 170 | 171 | val response: Future[HTML] = Expression { 172 | val phone = extract(lookupPhone(phoneString)) 173 | val address = extract(lookupAddress(phone)) 174 | val rep = extract(lookupReputation(phone)) 175 | renderPage(phone, address, rep) 176 | } 177 | 178 | or if you prefer even more magic, like so: 179 | 180 | import com.github.Expression 181 | import Expression.auto.extract 182 | 183 | val response: Future[HTML] = Expression { 184 | val phone = lookupPhone(phoneString) 185 | val address = lookupAddress(phone) 186 | val rep = lookupReputation(phone) 187 | renderPage(phone, address, rep) 188 | } 189 | 190 | ## Limitations 191 | 192 | Although the objective would be to support a maximum subset of the language, in practice we are far from there. Here are a bunch of constructs known to have good enough support as well as some known to have some issues. If a construct is in neither category it's safe to assume it probably has some issues. The hope is that once Macro support improves in Scala ([Scala Meta)](http://scalameta.org/), it will be easier to support all of the language. 193 | 194 | ### Know to work well enough 195 | 196 | - Function application 197 | - if-else statement 198 | - function currying 199 | - String interpolation 200 | - Blocks 201 | - basic `match` statements 202 | 203 | ### Known limitations 204 | 205 | - Does not support value pattern matching => `val (a,b) = something` 206 | - The `match` statement is a challenge to get right in all cases. It should work fine for basic usage, but will start to break down if using stable identifiers and excessive monadic behavior in pattern matches. Value pattern matching is actually desagured to a pattern match which is why it is not supported. 207 | 208 | ## How it works 209 | 210 | Expressions is based on a fairly straight forward transformation. 211 | 212 | Function applications within an `Expression` with arguments that contain an extract like this: 213 | 214 | Expression { foo(extract(a), b, extract(c) } 215 | 216 | are rewritten like so: 217 | 218 | instance.apply2(a,c)(foo(_,b,_)) 219 | 220 | where instance is either `Monad[A]` or `Applicative[A]` depending on what is available in scope 221 | 222 | Sometimes `bind` must be used in order to desugar the Expression: 223 | 224 | Expression { foo(extract(bar(extract(a))), b, extract(c)) } 225 | 226 | In this situation `bar` takes an `A` and produces a `Something[A]`. Here is the desugared code: 227 | 228 | instance.apply(instance.bind(a)(bar),c)(foo(_,b,_)) 229 | 230 | Match statement: 231 | 232 | Expression { 233 | extract(a) match { 234 | case 1 => extract(b) 235 | case 2 => extract(c) + 1 236 | } 237 | } 238 | 239 | becomes: 240 | 241 | instance.bind(a)(_ match { 242 | case 1 => b 243 | case 2 => instance.map(c)(cc => cc + 1) 244 | } 245 | 246 | Match statements can get hairy: 247 | 248 | Expression { 249 | val foo = extract(getFoo) 250 | extract(bar) match { 251 | case `foo` => extract(b) 252 | case 2 => extract(c) + 1 253 | } 254 | } 255 | 256 | becomes: 257 | 258 | { 259 | val foo = getFoo 260 | instance.bind(instance.apply(foo, bar)((_,_)){ case (x$1, x$2) => x$1 match { 261 | case `x$2` => b 262 | case 2 => instance.map(c)(cc => cc + 1) 263 | } 264 | } 265 | 266 | ## Similar Projects 267 | 268 | ### [Async/Await](https://github.com/scala/async) 269 | 270 | *Async/Await* is based on a language feature from C#. It basically does the same thing, but specialized for `Future`'s. My experimentation reveals that it is not possible to "fail-fast". It might be better optimized for runtime performance. 271 | 272 | ### [Effectful](https://github.com/pelotom/effectful) 273 | 274 | *Effectful* generalizes the idea behind Async/Await to any Monad. It currently does not use the least powerful interface and as such fundamentally does not support failing-fast for `Future`s. 275 | 276 | ### [Scala Workflow](https://github.com/aztek/scala-workflow) 277 | 278 | Scala Workflow is the most advanced project out there doing a similar thing. It uses the least powerful abstraction and also allows injecting functions available on the abstraction right in the middle of a workflow. Unfortunately, Scala Workflow requires untyped macros which are no longer supported in the latest version of Macro Paradise. It also does more heavy lifting then I believe is necessary. For instance it implements the implicit extraction on its own where as Expressions rely on good old Scala implicit resolution. This means that implicit extraction will break down in Expressions if the code has other unrelated implicit conversions (because Scala does not support multiple implicit conversions). IMHO this is a separate concern and I would rather see a macro or Scala itself support multiple implicit conversions with all the gotchas that come with it. So although Scala Workflow is in theory the most feature-full and generic solution, it has a possibly overly complicated implementation. 279 | 280 | ## TODO 281 | 282 | - Fix limitations 283 | - Implement `ctx` 284 | - Support for nested abstractions 285 | - Generalize tests 286 | - Currently the large majority of tests only test the `Option` context, there is no fundemental reason it could not be generalized to test many Contexts at the same time, although this would require some nifty changes to ScalaCheck or some alternative. See [how it's done in *Effectful*](https://github.com/pelotom/effectful/blob/master/src/test/scala/effectful/EffectfulSpec.scala#L18). 287 | - Make ScalaCheck function generation better 288 | - Currenlty, ScalaCheck only generates constant functions, this means the properties run a much higher risk of being false positives. It would be possible to create a few function generators for our purposes, but much better would be to fix [this issue](https://github.com/rickynils/scalacheck/issues/136) on [ScalaCheck](https://github.com/rickynils/scalacheck). 289 | - Use [Scala Meta](http://scalameta.org/) 290 | -------------------------------------------------------------------------------- /src/test/scala/IdiomBracketSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import org.scalacheck.Arbitrary 6 | import org.scalacheck.Arbitrary._ 7 | import org.specs2.ScalaCheck 8 | import org.specs2.mutable._ 9 | import Expression.{extract, bind2, bind3} 10 | import shapeless._ 11 | 12 | import scala.concurrent.Await 13 | import scalaz.{Monad, Applicative, Apply} 14 | import scalaz.std.option._ 15 | import scalaz.std.list._ 16 | import shapeless.contrib.scalaz._ 17 | 18 | class IdiomBracketSpec extends Specification with ScalaCheck { 19 | 20 | implicit def FutureArbitrary[A: Arbitrary]: Arbitrary[scala.concurrent.Future[A]] = 21 | Arbitrary(arbitrary[A] map ((x: A) => scala.concurrent.Future.successful(x))) 22 | 23 | type ThirteenOptions[A] = Option[A] :: 24 | Option[A] :: 25 | Option[A] :: 26 | Option[A] :: 27 | Option[A] :: 28 | Option[A] :: 29 | Option[A] :: 30 | Option[A] :: 31 | Option[A] :: 32 | Option[A] :: 33 | Option[A] :: 34 | Option[A] :: 35 | Option[A] :: HNil 36 | 37 | "IdiomBracket" >> { 38 | "function application" >> { 39 | "2 params" ! prop { (a: Option[String], b: Option[String], doThing: (String, String) => String) => 40 | val f = Expression.idiom[Option, String](doThing(extract[Option, String](a), extract[Option, String](b))) 41 | f ==== Apply[Option].apply2(a, b)(doThing) 42 | } 43 | "3 params" >> { 44 | "all of them extracts" ! prop { (a: Option[String], b: Option[String], c: Option[String], doThing: (String, String, String) => String) => 45 | val f = Expression.idiom[Option, String](doThing(extract(a), extract(b), extract(c))) 46 | f ==== Applicative[Option].apply3(a, b, c)(doThing) 47 | } 48 | "some extracts, some not" ! prop { (a: String, b: Option[String], c: Option[String], doThing: (String, String, String) => String) => 49 | val f = Expression.idiom[Option, String](doThing(a, extract(b), extract(c))) 50 | f ==== Applicative[Option].apply3(Some(a), b, c)(doThing) 51 | } 52 | // "so many parameters" ! prop { 53 | // (args: ThirteenOptions[String], 54 | // fun: (String, String, String, String, String, String, String, String, String, String, String, String, String) => String) => 55 | // val (a,b,c,d,e,f,g,h,i,j,k,l,m) = args.tupled 56 | // val result = IdiomBracket[Option, String](fun( 57 | // extract(a), 58 | // extract(b), 59 | // extract(c), 60 | // extract(d), 61 | // extract(e), 62 | // extract(f), 63 | // extract(g), 64 | // extract(h), 65 | // extract(i), 66 | // extract(j), 67 | // extract(k), 68 | // extract(l), 69 | // extract(m))) 70 | // result ==== Applicative[Option].map(sequence(a :: b :: c :: d :: e :: f :: g :: h :: i :: j :: k :: l :: m :: HNil)){ 71 | // case aa :: bb :: cc :: dd :: ee :: ff :: gg :: hh :: ii :: jj :: kk :: ll :: mm :: HNil => 72 | // fun(aa,bb,cc,dd,ee,ff,gg,hh,ii,jj,kk,ll,mm) 73 | // } 74 | // } 75 | } 76 | tag("type parameter") 77 | "function with type parameters" ! prop { (a: Option[String], fooImpl: String => String, b: String) => 78 | def foo[A](a: A, b: String): String = fooImpl(b) + a 79 | val f = Expression.idiom[Option, String](foo(b, extract(a))) 80 | f ==== Applicative[Option].map(a)(foo(b, _)) 81 | } 82 | } 83 | "method invocation" in { 84 | "no extract in LHS" ! prop { (a: String, b: Option[Int], c: Option[Int]) => 85 | val f = Expression.idiom[Option, Int](a.indexOf(extract(b), extract(c))) 86 | f ==== Applicative[Option].apply2(b, c)(a.indexOf(_, _)) 87 | } 88 | "extract in LHS" ! prop { (a: Option[String], b: Int, c: Option[Int]) => 89 | val f = Expression.idiom[Option, Int](extract(a).indexOf(b, extract(c))) 90 | f ==== Applicative[Option].apply2(a, c)(_.indexOf(b, _)) 91 | } 92 | "complex method invocation" in { 93 | "1" ! prop { (a: Option[String], b: Int, c: Option[Int], doThing: (String, String) => String) => 94 | val f = Expression.idiom[Option, Int](doThing(extract(a), extract(c).toString).indexOf(b, extract(c))) 95 | f ==== Applicative[Option].apply2(a, c)((aa, cc) => doThing(aa, cc.toString).indexOf(b, cc)) 96 | } 97 | "2" ! prop { (a: Option[String], b: Int, c: Option[Int], d: Option[String], doThing: (String, String) => String) => 98 | val f = Expression.idiom[Option, Int](doThing(extract(a), extract(d)).indexOf(b, extract(c))) 99 | f ==== Applicative[Option].apply3(a, c, d)((aa, cc, dd) => doThing(aa, dd).indexOf(b, cc)) 100 | } 101 | } 102 | tag("type parameter") 103 | "with type parameters" ! prop { (a: Option[Either[String, Int]]) => 104 | val f = Expression.idiom[Option, Int](extract(a).fold(_.length, identity)) 105 | f ==== Applicative[Option].map(a)(_.fold(_.length, identity)) 106 | } 107 | } 108 | "extract buried" in { 109 | "deep" ! prop { (a: Option[String], b: Option[String], c: Option[String], doThing: (String, String, String) => String, otherThing: String => String) => 110 | val f = Expression.idiom[Option, String](doThing(otherThing(extract(a)), extract(b), extract(c))) 111 | f ==== Applicative[Option].apply3(a, b, c)((aa, bb, cc) => doThing(otherThing(aa), bb, cc)) 112 | } 113 | "deeper" ! prop { (a: Option[String], b: Option[String], c: Option[String], doThing: (String, String, String) => String, otherThing: String => String, firstThis: String => String) => 114 | val f = Expression.idiom[Option, String](doThing(otherThing(firstThis(extract(a))), extract(b), extract(c))) 115 | f ==== Applicative[Option].apply3(a, b, c)((aa, bb, cc) => doThing(otherThing(firstThis(aa)), bb, cc)) 116 | } 117 | } 118 | tag("block") 119 | "block" in { 120 | "simple" ! prop { (a: Option[String], otherThing: String => String) => 121 | val f = Expression.idiom[Option, String] { 122 | otherThing(extract(a)) 123 | } 124 | f ==== Applicative[Option].map(a)(otherThing) 125 | } 126 | "slighly more complex is a useless way you would think" ! prop { (a: Option[String], otherThing: String => String) => 127 | val f = Expression.idiom[Option, String] { 128 | otherThing(extract(a)) 129 | otherThing(extract(a)) 130 | } 131 | f ==== Applicative[Option].map(a)(otherThing) 132 | } 133 | "pointless val" ! prop { (a: Option[String], otherThing: String => String) => 134 | val f = Expression.idiom[Option, String] { 135 | val aa = otherThing(extract(a)) 136 | aa 137 | } 138 | f ==== Applicative[Option].map(a)(otherThing) 139 | } 140 | "slightly less simple and somewhat useful" ! prop { (a: Option[String], otherThing: String => String) => 141 | val f = Expression.idiom[Option, String] { 142 | val aa = otherThing(extract(a)) 143 | otherThing(aa) 144 | } 145 | f ==== Applicative[Option].map(a)(aa => otherThing(otherThing(aa))) 146 | } 147 | /*"val pattern match" ! prop { (a: Option[String], test: String => (String, String)) => 148 | val f = IdiomBracket[Option, String] { 149 | val (first, second) = test(extract(a)) 150 | first + second 151 | } 152 | f ==== Applicative[Option].map(a){aa => 153 | val (first, second) = test(aa) 154 | first + second 155 | } 156 | }*/ 157 | tag("match") 158 | "match" in { 159 | "with extract in expression" ! prop { (a: Option[String]) => 160 | val f = Expression.idiom[Option, String] { 161 | extract(a) match { 162 | case "hello" => "h" 163 | case _ => "e" 164 | } 165 | } 166 | if (a.isDefined) 167 | f == Some(a.get match { 168 | case "hello" => "h" 169 | case _ => "e" 170 | }) 171 | else 172 | f == None 173 | } 174 | "with extract in RHS of case statement" ! prop { a: Option[String] => 175 | val f = Expression.idiom[Option, String] { 176 | List(1, 2, 3) match { 177 | case Nil => extract(a) + "!" 178 | case _ => "hello" 179 | } 180 | } 181 | val expected = List(1, 2, 3) match { 182 | case Nil => Applicative[Option].map(a)(_ + "!") 183 | case _ => Applicative[Option].pure("hello") 184 | } 185 | f ==== expected 186 | } 187 | "with stable identifier in pattern match case statement" ! prop { (a: Option[String], b: Option[String]) => 188 | val f = Expression.idiom[Option, String] { 189 | val bb = extract(b) 190 | extract(a) match { 191 | case `bb` => "h" 192 | case _ => "e" 193 | } 194 | } 195 | val expected = Applicative[Option].apply2(a, b)((a, b) => 196 | a match { 197 | case `b` => "h" 198 | case _ => "e" 199 | } 200 | ) 201 | f == expected 202 | } 203 | } 204 | "if statement" in { 205 | "extract in condition expression" ! prop { (a: Option[String]) => 206 | val f = Expression.idiom[Option, Int] { 207 | if (extract(a).length == 5) 10 else 20 208 | } 209 | f ==== Applicative[Option].map(a)(aa => if (aa.length == 5) 10 else 20) 210 | } 211 | } 212 | tag("funky") 213 | "renamed import" ! prop { (a: Option[String], b: Option[String], doThing: (String, String) => String) => 214 | import Expression.{extract => extractt} 215 | val f = Expression.idiom[Option, String](doThing(extractt(a), extractt(b))) 216 | f == Applicative[Option].apply2(a, b)(doThing) 217 | } 218 | tag("funky") 219 | "implicit extract" ! prop { (a: Option[String], b: Option[String], doThing: (String, String) => String) => 220 | import Expression.auto.extract 221 | val f = Expression.idiom[Option, String](doThing(a, b)) 222 | f == Applicative[Option].apply2(a, b)(doThing) 223 | } 224 | "with interpolated string" in { 225 | "simple" ! prop { (a: Option[String]) => 226 | val f = Expression.idiom[Option, String] { 227 | s"It’s ${extract(a)}!" 228 | } 229 | f ==== Applicative[Option].map(a)(aa => s"It’s $aa!") 230 | } 231 | "less simple" ! prop { (a: Option[String]) => 232 | 233 | case class Ok(message: String) 234 | def nameOfMonth(num: Int): Option[String] = a 235 | val month = 5 236 | 237 | val f = Expression.idiom[Option, Ok] { 238 | Ok(s"It’s ${extract(nameOfMonth(month.toInt))}!") 239 | } 240 | 241 | if (a.isDefined) 242 | f == Some(Ok(s"It’s ${nameOfMonth(month.toInt).get}!")) 243 | else 244 | f == None 245 | } 246 | } 247 | "with currying" in { 248 | "2 currys with one param" ! prop { (a: Option[String], test: String => String => String, c: String) => 249 | val f = Expression.idiom[Option, String](test(extract(a))(c)) 250 | f == Applicative[Option].map(a)(test(_)(c)) 251 | } 252 | "2 currys with one two params" ! prop { (a: Option[String], b: Option[String], test: (String, String) => String => String) => 253 | val f = Expression.idiom[Option, String](test(extract(a), extract(b))("foo")) 254 | f == Applicative[Option].apply2(a, b)(test(_, _)("foo")) 255 | } 256 | "2 currys with two two params" ! prop { (a: Option[String], b: Option[String], test: (String, String) => (String, String) => String) => 257 | val f = Expression.idiom[Option, String](test(extract(a), extract(b))("foo", "bar")) 258 | f == Applicative[Option].apply2(a, b)(test(_, _)("foo", "bar")) 259 | } 260 | "2 currys with extracts in both" ! prop { (a: Option[String], b: Option[String], test: String => String => String) => 261 | val f = Expression.idiom[Option, String](test(extract(a))(extract(b))) 262 | f ==== Applicative[Option].apply2(a, b)(test(_)(_)) 263 | } 264 | "2 currys with implicit" ! prop { (a: Option[String], b: String => String, c: String) => 265 | trait Proof 266 | def test(fst: String)(implicit snd: Proof): String = b(fst) 267 | implicit val myProof = new Proof {} 268 | val f = Expression.idiom[Option, String](test(extract(a))) 269 | f ==== Apply[Option].map(a)(test) 270 | } 271 | } 272 | "with tuples" ! prop { (a: Option[String], b: Option[String], test: String => String) => 273 | val f = Expression.idiom[Option, (String, String)] { 274 | (test(extract(a)), extract(b)) 275 | } 276 | f ==== Applicative[Option].apply2(a, b)((aa, bb) => (test(aa), bb)) 277 | } 278 | "with typed" in { 279 | "simple" ! prop { a: Option[String] => 280 | val f = Expression.idiom[Option, String](extract(a: Option[String])) 281 | f ==== a 282 | } 283 | "complex" ! prop { (a: Option[String], test: String => String) => 284 | val f = Expression.idiom[Option, String](test(extract(a)): String) 285 | f ==== a.map(test) 286 | } 287 | } 288 | "with List" ! prop { (a: List[String], b: List[String]) => 289 | val f = Expression[List, String](extract(a) + extract(b)) 290 | f == Applicative[List].apply2(a, b)(_ + _) 291 | } 292 | "with Future" ! prop { (a: scala.concurrent.Future[String], b: scala.concurrent.Future[String]) => 293 | import scala.concurrent.Future 294 | import scala.concurrent.ExecutionContext.Implicits.global 295 | import scala.concurrent.duration._ 296 | 297 | implicit val applicative: Applicative[Future] = scalaz.std.scalaFuture.futureInstance 298 | 299 | val f = Expression[Future, String](extract(a) + extract(b)) 300 | 301 | val timeout = FiniteDuration(100, TimeUnit.MILLISECONDS) 302 | Await.result(f, timeout) ==== Await.result(Applicative[Future].apply2(a, b)(_ + _), timeout) 303 | } 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/test/scala/CodeGeneration.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import scala.reflect.runtime.universe._ 4 | import scala.reflect.runtime.{currentMirror => cm} 5 | import scala.tools.reflect.{ToolBoxError, ToolBox} 6 | 7 | import org.specs2.mutable._ 8 | import com.github.jedesah.Expression.ContextSubset 9 | import utils._ 10 | 11 | class CodeGeneration extends Specification { sequential // The toolbox is not fully Thread safe sigh... 12 | 13 | class DefaultContext extends ContextSubset[reflect.runtime.universe.type] { 14 | var number = 0 15 | def freshName() = { 16 | number += 1 17 | "x" + number 18 | } 19 | def abort(pos: reflect.runtime.universe.Position, msg: String) = { throw new MacroAborted(msg) } 20 | def enclosingPosition= reflect.runtime.universe.NoPosition 21 | } 22 | 23 | case class MacroAborted(msg: String) extends Exception(msg) 24 | 25 | def compareAndPrintIfDifferent(actual: reflect.runtime.universe.Tree, expected: reflect.runtime.universe.Tree, compareString: Boolean = false) = { 26 | val areEqual = if(compareString) actual.toString == expected.toString else actual equalsStructure expected 27 | // There must be a better way of doing this! 28 | if (areEqual) true ==== true 29 | else { 30 | //println("ACTUAL RAW:\n" + showRaw(actual)) 31 | //println("EXPECTED RAW:\n" + showRaw(expected)) 32 | actual ==== expected 33 | } 34 | } 35 | 36 | def transformLast(block: reflect.runtime.universe.Tree, nbLines: Int = 1, monadic: Boolean = false) = { 37 | transformImpl(block, Last(nbLines), monadic) 38 | } 39 | 40 | def transformImpl(block: reflect.runtime.universe.Tree, range: Range, monadic: Boolean = false) = { 41 | val extractImport = q"import com.github.jedesah.Expression.extract" 42 | val tb = cm.mkToolBox() 43 | val everythingTyped = tb.typecheck(Block(extractImport :: block.children.init, block.children.last)) 44 | val lastLines = everythingTyped.children.drop(1).slice(range).toList 45 | val testAST = if(lastLines.size == 1)lastLines.head else Block(lastLines.init, lastLines.last) 46 | tb.untypecheck(Expression.transform(scala.reflect.runtime.universe)(new DefaultContext,testAST, q"App", everythingTyped.tpe, monadic).get) 47 | } 48 | 49 | def transform(block: reflect.runtime.universe.Tree, ignore: Int, monadic: Boolean = false) = { 50 | transformImpl(block, Drop(ignore), monadic) 51 | } 52 | 53 | "code generation" should { 54 | "simple function invocation" in { 55 | val ast = q""" 56 | def doThing(a: String, b: String) = ??? 57 | val a: Option[String] = ??? 58 | val b: Option[String] = ??? 59 | doThing(extract(a), extract(b)) 60 | """ 61 | val transformed = transformLast(ast) 62 | val expected = q""" 63 | App.apply2(a,b)(doThing) 64 | """ 65 | compareAndPrintIfDifferent(transformed, expected) 66 | } 67 | "complex method invocation" in { 68 | //IdiomBracket(doThing(extract(a), extract(c).toString).indexOf(b, extract(c))) 69 | val ast = q""" 70 | def doThing(a: String, b: String): String = ??? 71 | val a: Option[String] = ??? 72 | val b: Int = ??? 73 | val c: Option[Int] = ??? 74 | doThing(extract(a), extract(c).toString).indexOf(b, extract(c)) 75 | """ 76 | val transformed = transformLast(ast) 77 | val expected = q""" 78 | App.apply2( 79 | App.apply2( 80 | a, 81 | App.map(c)(x3 => x3.toString()) 82 | )(doThing), 83 | c 84 | )((x1, x2) => x1.indexOf(b,x2)) 85 | """ 86 | compareAndPrintIfDifferent(transformed, expected, compareString = true) 87 | } 88 | "recursive" in { 89 | val ast = q""" 90 | def doThing(a: String, b: String, c: String): String = ??? 91 | def otherThing(a: String): String = ??? 92 | val a,b,c: Option[String] = ??? 93 | doThing(otherThing(extract(a)),extract(b), extract(c)) 94 | """ 95 | val transformed = transformLast(ast) 96 | val expected = q""" 97 | App.apply3(App.map(a)(otherThing),b,c)(doThing) 98 | """ 99 | compareAndPrintIfDifferent(transformed, expected) 100 | } 101 | "with block" in { 102 | val ast = q""" 103 | def otherThing(a: String): String = a 104 | val a: Option[String] = Option("hello") 105 | val b: Option[String] = Some("h") 106 | val aa = otherThing(extract(a)) 107 | otherThing(aa) 108 | """ 109 | val transformed = transformLast(ast, nbLines = 2) 110 | val expected = q""" 111 | App.map(a)(x1 => {val aa = otherThing(x1); otherThing(aa)}) 112 | """ 113 | compareAndPrintIfDifferent(transformed, expected) 114 | } 115 | tag("match") 116 | "of match" in { 117 | "extract in RHS" in { 118 | val ast = q""" 119 | val a: Option[String] = ??? 120 | List(1,2,3) match { 121 | case Nil => extract(a) + "!" 122 | case _ => "hello" 123 | } 124 | """ 125 | val transformed = transformLast(ast) 126 | val expected = q""" 127 | App.map(a)(x1 => immutable.this.List.apply[Int](1,2,3) match { 128 | case immutable.this.Nil => x1.+("!") 129 | case _ => "hello" 130 | }) 131 | """ 132 | compareAndPrintIfDifferent(transformed, expected) 133 | }.pendingUntilFixed("Fails because Scala sucks at comparing ASTs") 134 | "with extract in LHS" in { 135 | "1" in { 136 | val ast = q""" 137 | val a: Option[String] = ??? 138 | extract(a) match { case "hello" => "h" } 139 | """ 140 | val transformed = transformLast(ast) 141 | val expected = q""" 142 | App.map(a)(((x1) => x1 match { 143 | case "hello" => "h" 144 | })) 145 | """ 146 | compareAndPrintIfDifferent(transformed, expected, compareString = false) 147 | } 148 | "2" in { 149 | val ast = q""" 150 | val a: Option[String] = ??? 151 | extract(a) match { 152 | case "hello" => "h" 153 | case _ => "e" 154 | } 155 | """ 156 | val transformed = transformLast(ast) 157 | val expected = q""" 158 | App.map(a)(((x1) => x1 match { 159 | case "hello" => "h" 160 | case _ => "e" 161 | })) 162 | """ 163 | compareAndPrintIfDifferent(transformed, expected, compareString = false) 164 | } 165 | "with stable identifier referring to extracted val" in { 166 | val ast = q""" 167 | val a: Option[String] = ??? 168 | val b: Option[String] = ??? 169 | val bb = extract(b) 170 | extract(a) match { 171 | case `bb` => "h" 172 | case _ => "e" 173 | } 174 | """ 175 | val transformed = transformLast(ast, nbLines = 2) 176 | val expected = q""" 177 | val bb = b 178 | App.apply2(a,bb)((x2,x1) => x2 match { 179 | case `x1` => "h" 180 | case _ => "e" 181 | }) 182 | """ 183 | compareAndPrintIfDifferent(transformed, expected) 184 | } 185 | } 186 | } 187 | "if statement" in { 188 | val ast = q""" 189 | val a: Option[String] = ??? 190 | if (extract(a).length == 5) 10 else 20 191 | """ 192 | val transformed = transformLast(ast) 193 | val expected = q""" 194 | App.map(a)(x1 => if(x1.length().==(5)) 10 else 20) 195 | """ 196 | compareAndPrintIfDifferent(transformed, expected) 197 | } 198 | tag("interpolated string") 199 | "interpolated string" in { 200 | val ast = q""" 201 | val a: Option[String] = ??? 202 | s"It's $${extract(a)}!" 203 | """ 204 | val transformed = transformLast(ast) 205 | val expected = q""" 206 | App.map(a)(((x1) => scala.StringContext.apply("It\'s ", "!").s(x1))) 207 | """ 208 | compareAndPrintIfDifferent(transformed, expected, compareString = true) 209 | } 210 | "with currying" in { 211 | val ast = q""" 212 | def test(a: String)(b: String): String = ??? 213 | val a: Option[String] = ??? 214 | test(extract(a))("foo") 215 | """ 216 | val transformed = transformLast(ast) 217 | val expected = q""" 218 | App.map(a)(x1 => test(x1)("foo")) 219 | """ 220 | compareAndPrintIfDifferent(transformed, expected, compareString = true) 221 | } 222 | "with typed" in { 223 | val ast = q""" 224 | def test(a: String): String = ??? 225 | val a: Option[String] = ??? 226 | test(extract(a)): String 227 | """ 228 | val transformed = transformLast(ast) 229 | val expected = q""" 230 | App.map(a)(test): Option[String] 231 | """ 232 | compareAndPrintIfDifferent(transformed, expected, compareString = true) 233 | }.pendingUntilFixed("not sure how to pass in the type that is an Applicative directly to the genreation function") 234 | "asc reverse core site" in { 235 | val ast = q""" 236 | val phone: Option[String] = ??? 237 | val hitCounter: Option[String] = ??? 238 | val locById: Option[String] = ??? 239 | def test(a: String, b: String): Option[(String, String)] = ??? 240 | def otherTest(a: String, b: String, c: String): Option[String] = ??? 241 | val tuple: (String, String) = extract(test(extract(phone), extract(hitCounter))) 242 | extract(otherTest(tuple._2, tuple._1, extract(locById))) 243 | """ 244 | val transformed = transformLast(ast, monadic = true, nbLines = 2) 245 | val expected = q""" 246 | val tuple = App.bind2(phone, hitCounter)(test) 247 | App.bind3(App.map(tuple)(_._1), App.map(tuple)(_._2), locById)(otherTest) 248 | """ 249 | compareAndPrintIfDifferent(transformed, expected) 250 | }.pendingUntilFixed("Don't know how to make this a deterministic test") 251 | "val definition pattern matching" in { 252 | val ast = q""" 253 | val a: Option[String] = ??? 254 | def test(a: String): (String, String) = ??? 255 | val (first, second) = test(extract(a)) 256 | first + second 257 | """ 258 | val transformed = transform(ast, monadic = false, ignore = 2) 259 | val expected = q""" 260 | App.map(a){ x1 => 261 | val (first, second) = test(x1) 262 | first + second 263 | } 264 | """ 265 | compareAndPrintIfDifferent(transformed, expected) 266 | }.pendingUntilFixed("It's quite hard to support pattern matching value definitions because of the complex desaguring involved (which happens before the Tree is passed into the macro)") 267 | "monadic block" in { 268 | val ast = q""" 269 | val a: Option[String] = ??? 270 | def foo(a: String): Option[String] = ??? 271 | def bar(a: String): String = ??? 272 | val b = foo(extract(a)) 273 | bar(extract(b)) 274 | """ 275 | val transformed = transformLast(ast, monadic = true, nbLines = 2) 276 | val expected = q""" 277 | val b = App.map(a)(foo) 278 | App.map(App.join(b))(bar) 279 | """ 280 | compareAndPrintIfDifferent(transformed, expected) 281 | } 282 | "iGraph options stuff" in { 283 | val ast = q""" 284 | trait LocationType 285 | case class City(city: Option[String]) extends LocationType 286 | case class Country(countryName: Option[String]) extends LocationType 287 | case object State extends LocationType 288 | val loc: LocationType = City(None) 289 | // 'EN-US' will be "normalized" to 'en' 290 | val userLocale: String = ??? 291 | def normalize(locale: String) = locale.take(2).toLowerCase 292 | val namesOption: Option[Map[String, String]] = ??? 293 | val names = extract(namesOption) 294 | val normalizedMap = names.map { case (key, value) => (normalize(key), value)} 295 | val name = extract(normalizedMap.get(normalize(userLocale)).orElse(normalizedMap.get("en"))) 296 | // If there is no user requested locale or english, leave Location unchanged 297 | loc match { 298 | case loc:City => loc.copy(city = Some(name)) 299 | case loc:Country => loc.copy(countryName = Some(name)) 300 | case _ => loc 301 | } 302 | """ 303 | val transformed = transform(ast, monadic = true, ignore = 10) 304 | val expected = q""" 305 | val names = namesOption 306 | val normalizedMap = App.map(names)(x1 => x1.map { case (key, value) => (normalize(key), value)}) 307 | val name = App.bind(normalizedMap)(x2 => x2.get(normalize(userLocale)).orElse(normalizeMap.get("en"))) 308 | loc match { 309 | case loc: City => App.map(name)(x3 => loc.copy(city = Some(x3))) 310 | case loc: Country => App.map(name)(x4 => loc.copy(countryName = Some(x4))) 311 | case _ => App.pure(loc) 312 | } 313 | """ 314 | compareAndPrintIfDifferent(transformed, expected) 315 | }.pendingUntilFixed("This is pretty close, and works in the actual Spec for now") 316 | "canBuildFrom" in { 317 | val ast = q""" 318 | val a: Option[List[String]] = ??? 319 | def b: String => String = ??? 320 | extract(a).map(b) 321 | """ 322 | val transformed = transform(ast, ignore = 2) 323 | val expected = q""" 324 | App.map(a)(x1 => x1.map[String, List[String]](b)(immutable.this.List.canBuildFrom[String])) 325 | """ 326 | compareAndPrintIfDifferent(transformed, expected, compareString = true) 327 | } 328 | "implicit currying" in { 329 | val ast = q""" 330 | val a: Option[String] = ??? 331 | val b: String => String = ??? 332 | val c: String = ??? 333 | trait Proof 334 | def test(fst: String)(implicit snd: Proof): String = b(fst) 335 | implicit val myProof = new Proof {} 336 | test(extract(a)) 337 | """ 338 | val transformed = transform(ast, ignore = 6) 339 | val expected = q""" 340 | App.map(a)(x1 => test(x1)(myProof)) 341 | """ 342 | compareAndPrintIfDifferent(transformed, expected) 343 | } 344 | "SIP-22 example" in { 345 | val ast = q""" 346 | val optionDOY: Option[String] = ??? 347 | val date = "(\\d+)/(\\d+)".r 348 | case class Ok(message: String) 349 | case class NotFound(message: String) 350 | def nameOfMonth(num: Int): Option[String] = None 351 | extract (optionDOY) match { 352 | case date(month, day) => 353 | Ok(s"It’s $${extract(nameOfMonth(month.toInt))}!") 354 | case _ => 355 | NotFound("Not a date, mate!") 356 | } 357 | """ 358 | val transformed = transform(ast, ignore = 7, monadic = true) 359 | val expected = q""" 360 | App.bind(optionDOY){ doy => 361 | doy match { 362 | case date(month, day) => 363 | App.map(nameOfMonth(month.toInt))(x1 => Ok(s"It's $$x1!")) 364 | case _ => 365 | NotFound("Not a date, mate!") 366 | } 367 | } 368 | """ 369 | compareAndPrintIfDifferent(transformed, expected) 370 | }.pendingUntilFixed("Close Enough") 371 | "clean compilation error if monad is required" in { 372 | val ast = q""" 373 | val optionDOY: Option[String] = ??? 374 | val date = "(\\d+)/(\\d+)".r 375 | case class Ok(message: String) 376 | case class NotFound(message: String) 377 | def nameOfMonth(num: Int): Option[String] = None 378 | extract (optionDOY) match { 379 | case date(month, day) => 380 | Ok(s"It’s $${extract(nameOfMonth(month.toInt))}!") 381 | case _ => 382 | NotFound("Not a date, mate!") 383 | } 384 | """ 385 | transform(ast, ignore = 7) must throwA[MacroAborted](message = "This expression requires an instance of Monad") 386 | } 387 | "so many parameters" in { 388 | val ast = q""" 389 | val fun: (String, String, String, String, String, String, String, String, String, String, String, String, String) => String = ??? 390 | val a: Option[String] = ??? 391 | val b: Option[String] = ??? 392 | val c: Option[String] = ??? 393 | val d: Option[String] = ??? 394 | val e: Option[String] = ??? 395 | val f: Option[String] = ??? 396 | val g: Option[String] = ??? 397 | val h: Option[String] = ??? 398 | val i: Option[String] = ??? 399 | val j: Option[String] = ??? 400 | val k: Option[String] = ??? 401 | val l: Option[String] = ??? 402 | val m: Option[String] = ??? 403 | fun( 404 | extract(a), 405 | extract(b), 406 | extract(c), 407 | extract(d), 408 | extract(e), 409 | extract(f), 410 | extract(g), 411 | extract(h), 412 | extract(i), 413 | extract(j), 414 | extract(k), 415 | extract(l), 416 | extract(m)) 417 | """ 418 | val expected = q""" 419 | App.map(sequence(x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: x7 :: x8 :: x9 :: x10 :: x11 :: x12 :: x13 :: HNil)) { 420 | case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: x7 :: x8 :: x9 :: x10 :: x11 :: x12 :: x13 :: HNil => 421 | fun(x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13) 422 | } 423 | """ 424 | compareAndPrintIfDifferent(transformLast(ast), expected) 425 | }.pendingUntilFixed("Don't have time to figure out HList problem right now") 426 | } 427 | } -------------------------------------------------------------------------------- /src/main/scala/Expression.scala: -------------------------------------------------------------------------------- 1 | package com.github.jedesah 2 | 3 | import scala.language.experimental.macros 4 | 5 | import scala.annotation.compileTimeOnly 6 | import scalaz._ 7 | import scala.collection.mutable.ListBuffer 8 | 9 | object Expression { 10 | 11 | def bind[F[_]:Monad,A,B](a: F[A])(f: A => F[B]): F[B] = { 12 | val i = implicitly[Monad[F]] 13 | i.bind(a)(f) 14 | } 15 | 16 | def bind2[F[_]:Monad,A,B,C](a: F[A], b: F[B])(f: (A,B) => F[C]): F[C] = { 17 | val i = implicitly[Monad[F]] 18 | i.bind(i.apply2(a, b)((_, _))) { 19 | f.tupled 20 | } 21 | } 22 | 23 | def bind3[F[_]:Monad,A,B,C,D](a: F[A], b: F[B], c: F[C])(f: (A,B,C) => F[D]): F[D] = { 24 | val i = implicitly[Monad[F]] 25 | i.bind(i.apply3(a, b, c)((_, _, _))) { 26 | f.tupled 27 | } 28 | } 29 | 30 | val merelyLiftedMsg = "Expression was merely lifter, there is a probably a better more explicit way of achieving the same result" 31 | 32 | def apply[F[_]:Applicative,T](x: T): F[T] = macro adaptive[T,F] 33 | def idiom[F[_]:Applicative,T](x: T): F[T] = macro idiomBracket[T,F] 34 | //def apply2[T](x: T): Option[Option[T]] = macro idiomBracket2[T] 35 | def monad[F[_]: Monad,T](x: T): F[T] = macro doNotation[T,F] 36 | 37 | @compileTimeOnly("`extract` must be enclosed in an `Expression`") 38 | def extract[F[_], T](applicative: F[T]): T = sys.error(s"extract should have been removed by macro expansion!") 39 | 40 | // I do not know why I need this... It has to do with the reflective toolbox 41 | @compileTimeOnly("`extract` must be enclosed in an `Expression`") 42 | def extract[T](applicative: Option[T]): T = sys.error(s"extract should have been removed by macro expansion!") 43 | 44 | object auto { 45 | @compileTimeOnly("`extract` must be enclosed in an `Expression`") 46 | implicit def extract[F[_], T](option: F[T]): T = sys.error(s"extract should have been removed by macro expansion!") 47 | } 48 | 49 | import scala.reflect.macros.blackbox.Context 50 | 51 | def doNotation[T, F[_]](c: Context)(x: c.Expr[T])(instance: c.Expr[Monad[F]])(implicit tag: c.WeakTypeTag[F[_]]): c.Expr[F[T]] = { 52 | macroImpl(c)(x,instance.tree, tag, monadic = true) 53 | } 54 | 55 | def idiomBracket[T: c.WeakTypeTag, F[_]](c: Context)(x: c.Expr[T])(instance: c.Expr[Applicative[F]])(implicit tag: c.WeakTypeTag[F[_]]): c.Expr[F[T]] = { 56 | macroImpl(c)(x,instance.tree,tag, monadic = false) 57 | } 58 | 59 | def adaptive[T, F[_]](c: Context)(x: c.Expr[T])(instance: c.Expr[Applicative[F]])(implicit tag: c.WeakTypeTag[F[_]]): c.Expr[F[T]] = { 60 | import c.universe._ 61 | val monadic = instance.tree.tpe.toString.contains("Monad") 62 | macroImpl(c)(x, instance.tree,tag, monadic) 63 | } 64 | 65 | def macroImpl[T, F[_]](c: Context)(x: c.Expr[_], instance: c.universe.Tree, tag: c.WeakTypeTag[_], monadic: Boolean) = { 66 | import c.universe._ 67 | val result = transform(c.universe)(ContextSubset(c),x.tree, instance, tag.tpe, monadic) 68 | if (!result.isDefined) c.warning(c.enclosingPosition, merelyLiftedMsg) 69 | c.Expr[F[T]](c.untypecheck(result.getOrElse(q"$instance.pure(${x.tree})"))) 70 | } 71 | 72 | /*def idiomBracket2[T: c.WeakTypeTag](c: Context)(x: c.Expr[T]): c.Expr[Option[Option[T]]] = { 73 | import c.universe._ 74 | val result = transformAST(c.universe, c)(x.tree, q"scalaz.Applicative[Option]", monadic = false) 75 | if (!result.isDefined) c.warning(c.enclosingPosition, merelyLiftedMsg) 76 | c.Expr[Option[Option[T]]](result.getOrElse(q"Some(Some($x.tree))")) 77 | }*/ 78 | 79 | trait ContextSubset[U <: scala.reflect.api.Universe] { 80 | def freshName(): String 81 | def abort(pos: U#Position, msg: String): Nothing 82 | def enclosingPosition: U#Position 83 | } 84 | 85 | object ContextSubset { 86 | def apply(c: Context) = new ContextSubset[c.universe.type] { 87 | def freshName(): String = c.freshName() 88 | def abort(pos: c.Position, msg: String): Nothing = c.abort(pos, msg) 89 | def enclosingPosition: c.Position = c.enclosingPosition 90 | } 91 | } 92 | 93 | /** 94 | * 95 | * @param u The universe of the Trees. Required to operate under current Scala reflection architecture. Trees cannot 96 | * exist without a universe. 97 | * @param c Context to use. Typically supplied by the macro definition 98 | * @param ast AST to transform 99 | * @return Some(Tree) if the tree was transformed or none if it was not transformed 100 | */ 101 | def transform(u: scala.reflect.api.Universe)(c: ContextSubset[u.type], ast: u.Tree, instance: u.Tree, instanceType: u.Type, monadic: Boolean): Option[u.Tree] = { 102 | import u._ 103 | 104 | /** 105 | * Lifts the following expression to an Applicative either by removing an extract function (it can be deeply nested) 106 | * or by simply adding a call to the pure function of the applicativeInstance if the expression contained no extract 107 | * functions. 108 | * @param expr Expression to be lifted by an AST transformation 109 | * @return New expression that has been lifted 110 | */ 111 | def lift(expr: u.Tree, flatten: Boolean = false): (u.Tree, Int) = { 112 | def wrapInFunctionAndApply(expr: u.Tree, args: List[u.Tree], namesInExpr: List[u.TermName]) = { 113 | if (args.size > 12) { 114 | // a :: b :: c 115 | val argsWithCons = args.reduceLeft((leftArg, rightArg) => q"$leftArg :: $rightArg") 116 | // a :: b :: c :: HNil 117 | val hlist = q"$argsWithCons :: shapeless.HNil" 118 | val namesWithCons = namesInExpr.map(Ident(_)).reduceLeft[u.Tree]((leftArg, rightArg) => q"$leftArg :: $rightArg") 119 | val namesHList = q"$namesWithCons :: shapeless.HNil" 120 | (q"$instance.map(shapeless.contrib.scalaz.SequenceFunctions.sequence($hlist)){ case $namesHList => $expr}", 1) 121 | } else { 122 | val lambda = createFunction(expr, namesInExpr) 123 | wrapInApply(lambda, args) 124 | } 125 | } 126 | def wrapInApply(expr: u.Tree, args: List[u.Tree]) = { 127 | val applyTerm = getApplyTerm(args.length, flatten) 128 | if (!flatten) (q"$instance.$applyTerm(..$args)($expr)", 1) 129 | else (q"com.github.jedesah.Expression.$applyTerm(..$args)($expr)($instance)",1) 130 | } 131 | expr match { 132 | case fun: Apply if isExtractFunction(fun) => 133 | val extracted = fun.args(0) 134 | if (hasExtracts(extracted) && !monadic) c.abort(c.enclosingPosition, "It is not possible to lift nested extracts in non monadic context") 135 | // directly nested extracts: extract(extract(a)) 136 | if (isExtractFunction(extracted)) { 137 | val lifted = lift(extracted)._1 138 | (q"$instance.join($lifted)",1) 139 | } 140 | else if (hasExtracts(extracted)) { 141 | lift(extracted, true) 142 | } else { 143 | (extracted, 1) 144 | } 145 | case _ if !hasExtracts(expr) => (q"$instance.pure($expr)", 1) 146 | // An expression of the form: 147 | // a match { case "bar" => extract(a); case _ => extract(b) } 148 | // can be rewritten simply as 149 | // a match { case "bar" => a; case _ => b } 150 | // no need to include the match within the mapping 151 | // This has the added benefit in the case of the Future monad of not blocking if we fall into a case pattern 152 | // that does not depend on a Future 153 | case Match(expr, cases) if !hasExtracts(expr) && cases.forall{ case cq"$x1 => $anything" => !hasExtracts(x1)} => 154 | val newCases = cases.map{ case cq"$wtv => $x2" => cq"$wtv => ${lift(x2)._1}"} 155 | (Match(expr, newCases), 1) 156 | // This is handling the case where all the arguments need to be extracted so to produce the following transformation 157 | // test(extract(a)) => App.map(a)(test) 158 | // since scalaz only has up to apply12, we cannot use this strategy if there are 13 or more arguments to the function application 159 | case Apply(ident@Ident(_), args) if args.forall(hasExtracts) && args.size < 13 => 160 | wrapInApply(ident, args.map(lift(_)._1)) 161 | case _ if nbExtracts(expr) == 1 => 162 | val (newExpr, replaced) = replaceExtractsWithRef(expr, insidePatternMatch = false) 163 | val List((name, arg)) = replaced 164 | val liftedArg = lift(arg)._1 165 | wrapInFunctionAndApply(newExpr, List(liftedArg), List(name)) 166 | case app: Apply => 167 | // we need to go case by case for each argument and see if we need to extract it 168 | val (transformedApply, replacements) = replaceExtractWithRefApply(app) 169 | // Names are the nam 170 | val (lambdaArgumentNames, argsWithExtracts) = replacements.unzip 171 | wrapInFunctionAndApply(transformedApply, argsWithExtracts.map(lift(_)._1), lambdaArgumentNames) 172 | // Not sure yet how to handle case with direct nested extracts 173 | // Currently will error because of check in first case 174 | /*else { 175 | val names: List[u.TermName] = List.fill(liftedArgs.size)(c.freshName()).map(TermName(_)) 176 | val transformedArgs = liftedArgs.zip(names).map { case (arg, name) => 177 | val ident = Ident(name) 178 | if (hasExtracts(arg)) ident 179 | else q"$applicativeInstance.pure($ident)" 180 | } 181 | val inner = createFunction(q"$applicativeInstance.$applyTerm(..$transformedArgs)($ref)", names) 182 | val reLiftedArgs = liftedArgs.map(lift(_)) 183 | (q"$applicativeInstance.$applyTerm(..$reLiftedArgs)($inner)", 2) 184 | }*/ 185 | case Block(exprs, finalExpr) => { 186 | var arityLastTransform: Int = 0 187 | val newExprs = (exprs :+ finalExpr).foldLeft[(Map[String, Int], List[u.Tree])]((Map(), Nil)) { (accu, expr) => 188 | val (names, exprs) = accu 189 | expr match { 190 | // We need to remember the name of the value definition so that we can add extract methods later so that the right thing happens 191 | case ValDef(mods, name, tpt, rhs) => 192 | val (tRHS, transformArity) = lift(addExtractR(rhs, names)) 193 | arityLastTransform = transformArity 194 | (names + (name.toString -> transformArity), exprs :+ ValDef(mods, name, TypeTree(), tRHS)) 195 | // If it's just an identifier, let's leave it as is but reconstruct it so that it looses it's type. 196 | case ident: Ident => 197 | arityLastTransform = names(ident.name.toString) 198 | (names, exprs :+ Ident(TermName(ident.name.toString))) 199 | // Anything else, we need to add extracts to identifiers of transformed `ValDef`s because we lifted the type of the symbol they refer to. 200 | case _ => 201 | val (transformed, transformArity) = lift(addExtractR(expr, names)) 202 | arityLastTransform = transformArity 203 | (names, exprs :+ transformed) 204 | } 205 | }._2 206 | (Block(newExprs.init, newExprs.last), arityLastTransform) 207 | } 208 | // TODO: Figure out why unchanged case pattern seems to go bonky in macro 209 | case Match(expr, cases) => 210 | 211 | if (hasExtracts(expr)) { 212 | val requireMonad = cases.exists{ case cq"$x1 => $x2" => 213 | val bindingsToLift = x1.collect{ case Bind(name, _) => name} 214 | val (_, replacements) = replaceExtractsWithRef(x2, false) 215 | val (_, exprsToLift) = replacements.unzip 216 | exprsToLift.exists( expr => expr.exists{ 217 | case Ident(name) => bindingsToLift.exists(_ == name) 218 | case _ => false 219 | }) 220 | } 221 | if (requireMonad && !monadic) c.abort(c.enclosingPosition, "This expression requires an instance of Monad") 222 | } 223 | // My current implementation is going to assume there is no stable identifier in the pattern matches 224 | // and if there is, it will fall back to not using Monad 225 | if (monadic && cases.forall{case cq"$x1 => $wtv" => !hasExtracts(x1)}) { 226 | val newCases = cases.map{case cq"$wtv => $x2" => cq"$wtv => ${lift(x2)._1}"} 227 | val liftedExpr = lift(expr)._1 228 | val exprName = TermName(c.freshName()) 229 | val newExpr = Ident(exprName) 230 | val function = createFunction(q"$newExpr match { case ..$newCases}", List(exprName)) 231 | (q"$instance.bind($liftedExpr)($function)",1) 232 | } else { 233 | val (tCases, argsWithWhatTheyReplace: List[List[(u.TermName, u.Tree)]]@unchecked) = cases.map { case cq"$x1 => $x2" => 234 | val (newX1, argsWithWhatTheyReplace1) = replaceExtractsWithRef(x1, insidePatternMatch = true) 235 | val (newX2, argsWithWhatTheyReplace2) = replaceExtractsWithRef(x2, insidePatternMatch = false) 236 | (cq"$newX1 => $newX2", argsWithWhatTheyReplace1 ++ argsWithWhatTheyReplace2) 237 | }.unzip 238 | val (names, args) = argsWithWhatTheyReplace.flatten.unzip 239 | // Add the expression to the arguments being transformed if it contains an extract 240 | val (allArgs, newExpr, allNames) = if (hasExtracts(expr)) { 241 | val exprName = TermName(c.freshName()) 242 | (expr :: args, Ident(exprName), exprName :: names) 243 | } else (args, expr, names) 244 | wrapInFunctionAndApply(q"$newExpr match { case ..$tCases}", allArgs.map(lift(_)._1), allNames) 245 | } 246 | case ifExpr@If(expr, trueCase, falseCase) => 247 | if (!monadic) { 248 | val (withExtractsRemoved, substitutions) = replaceExtractWithRefIf(ifExpr) 249 | wrapInFunctionAndApply(withExtractsRemoved, substitutions.values.toList.map(lift(_)._1), substitutions.keys.toList) 250 | } 251 | else { 252 | val List(exprT, trueCaseT, falseCaseT) = 253 | if (flatten) List(lift(expr)._1, trueCase, falseCase) 254 | else List(expr, trueCase, falseCase).map(lift(_)._1) 255 | (q"$instance.bind($exprT)(if(_) $trueCaseT else $falseCaseT)", 1) 256 | } 257 | // extract(aa).foo 258 | case Select(qual, name) => 259 | val lifted = lift(qual)._1 260 | (q"$instance.map($lifted)(_.${name.toTermName})",1) 261 | case Typed(expr, typeName) => 262 | // TODO: This possibly not the most robust way of doing things, but it works for now 263 | val result = AppliedTypeTree(Ident(TypeName(instanceType.toString)),List(typeName)) 264 | (Typed(lift(expr)._1, result),1) 265 | case _ => throw new AssertionError(s"An extract remains in this expression: $expr, but I don't know how to get rid of it, I am sorry...") 266 | } 267 | } 268 | 269 | def getApplyTerm(arity: Int, flatten: Boolean = false) = { 270 | if (arity > 12) 271 | c.abort(c.enclosingPosition, "scalaz does not define an apply13 or more which is necessary of our rewrite to work. Reformat your code to avoid functions receiving more than 12 parameters.") 272 | val applyFunName = 273 | if (flatten) 274 | if (arity == 1) s"bind" else s"bind$arity" 275 | else 276 | if (arity == 1) "map" else s"apply$arity" 277 | TermName(applyFunName) 278 | } 279 | 280 | def createFunction(rhs: u.Tree, args: List[u.TermName]) = { 281 | val lhs = args.map( name => ValDef(Modifiers(Flag.PARAM), name, TypeTree(), EmptyTree)) 282 | Function(lhs, rhs) 283 | } 284 | 285 | def replaceExtractWithRefApply(app: u.Apply): (u.Tree, List[(u.TermName, u.Tree)]) = { 286 | val namesWithReplaced = ListBuffer[(u.TermName, u.Tree)]() 287 | val newFun = if (hasExtracts(app.fun)) { 288 | val name = TermName(c.freshName()) 289 | app.fun match { 290 | case Select(ref, methodName) => 291 | namesWithReplaced += ((name, ref)) 292 | Select(Ident(name), methodName) 293 | case innerApp: TypeApply => 294 | // I am going to assume for now that a TypeApply only has a Select as a function 295 | val Select(ref, methodName) = innerApp.fun 296 | namesWithReplaced += ((name, ref)) 297 | Select(Ident(name), methodName) 298 | // Here we are handling currying 299 | case innerApp: Apply => 300 | // test(extract(a))("foo", "bar") => App.map(a)(x1 => test(x1)("foo", "bar")) 301 | if (app.args.forall(!hasExtracts(_))) { 302 | val (newAst, args) = replaceExtractWithRefApply(innerApp) 303 | return (Apply(newAst, app.args), args) 304 | } 305 | else { 306 | namesWithReplaced += ((name, innerApp)) 307 | Ident(name) 308 | } 309 | } 310 | } else app.fun 311 | val newArgs = app.args.map { arg => 312 | if (hasExtracts(arg)) { 313 | val name = TermName(c.freshName()) 314 | namesWithReplaced += ((name, arg)) 315 | Ident(name) 316 | } else arg 317 | } 318 | (Apply(newFun, newArgs), namesWithReplaced.toList) 319 | } 320 | 321 | def replaceExtractWithRefIf(ifElse: u.If): (u.Tree, Map[u.TermName,u.Tree]) = { 322 | val substitutions = collection.mutable.Map[u.TermName, u.Tree]() 323 | val List(newCondition, newThen, newElse) = List(ifElse.cond, ifElse.thenp, ifElse.elsep).map { expr => 324 | if (hasExtracts(expr)){ 325 | val name = TermName(c.freshName()) 326 | substitutions += ((name, expr)) 327 | Ident(name) 328 | } else expr 329 | } 330 | (If(newCondition, newThen, newElse), substitutions.toMap) 331 | } 332 | 333 | /** 334 | * It is smart enough to detect extracts that are on the left hand side of a pattern match and do the appropriate 335 | * thing which is to make sure the identifier replacing the expression is a "stable" identifier. 336 | * @param expr The expression from which to replace any extract with an identifier 337 | * @param insidePatternMatch Whether we are inside of a pattern match. If we are inside a pattern match, we need 338 | * to use a stable identifier in order for the transformed code to have the same meaning 339 | * as the original code because an identifier in a pattern match is being assigned too 340 | * if not indicated as a stable identifier that refers to some stable value outside 341 | * the pattern match 342 | * @return The transformed tree along with a list of new identifiers and the expressions they replace 343 | * (including the original extract function) 344 | * Pitfall: It would be tempting to remove the extract here but removing the extract should be left 345 | * to more specialized code because there are few corner cases to consider. 346 | */ 347 | def replaceExtractsWithRef(expr: u.Tree, insidePatternMatch: Boolean): (u.Tree, (List[(u.TermName,u.Tree)])) = { 348 | val namesWithReplaced = ListBuffer[(u.TermName, u.Tree)]() 349 | def impl(expr: u.Tree, insidePatternMatch: Boolean): u.Tree = { 350 | object ReplaceExtract extends Transformer { 351 | override def transform(tree: u.Tree): u.Tree = tree match { 352 | case fun: Apply if isExtractFunction(fun) => 353 | val name = TermName(c.freshName()) 354 | namesWithReplaced += ((name, fun)) 355 | if (insidePatternMatch) q"`$name`" else Ident(name) 356 | case cq"$x1 => $x2" => 357 | assert(!insidePatternMatch) 358 | cq"${impl(x1, true)} => ${impl(x2, false)}" 359 | case _ => super.transform(tree) 360 | } 361 | } 362 | ReplaceExtract.transform(expr) 363 | } 364 | (impl(expr, insidePatternMatch), namesWithReplaced.toList) 365 | } 366 | 367 | def addExtractR(expr: u.Tree, names: Map[String, Int]): u.Tree = { 368 | object AddExtract extends Transformer { 369 | override def transform(tree: u.Tree): u.Tree = tree match { 370 | case ident@Ident(name) => { 371 | val untypedIdent = Ident(TermName(name.toString)) 372 | if (names.keys.toList.contains(name.toString)) 373 | (0 until names(name.toString)).foldLeft[u.Tree](untypedIdent)((tree, _) => addExtract(tree)) 374 | else ident 375 | } 376 | case _ => super.transform(tree) 377 | } 378 | } 379 | AddExtract.transform(expr) 380 | } 381 | 382 | def addExtract(expr: u.Tree): u.Tree = { 383 | q"com.github.jedesah.Expression.extract($expr)" 384 | } 385 | 386 | def nbExtracts(expr: u.Tree): Int = expr.filter(isExtractFunction).size 387 | 388 | def hasExtracts(expr: u.Tree): Boolean = expr.exists(isExtractFunction) 389 | 390 | def isExtractFunction(tree: u.Tree): Boolean = { 391 | val expressionPath = "com.github.jedesah.Expression" 392 | val extractMethodNames = List(s"$expressionPath.extract", s"$expressionPath.auto.extract") 393 | tree match { 394 | case extract: Apply if extract.symbol != null && extractMethodNames.contains(extract.symbol.fullName) => true 395 | case q"com.github.jedesah.Expression.extract($_)" => true 396 | case _ => false 397 | } 398 | } 399 | 400 | def isDoubleExtract(expr: u.Tree) = if (isExtractFunction(expr)) isExtractFunction(expr.asInstanceOf[Apply].args(0)) else false 401 | 402 | if (ast.exists(isDoubleExtract)) c.abort(c.enclosingPosition, "It is not possible to lift directly nested extracts") 403 | val (result, transformArity) = lift(ast) 404 | if (transformArity == 0) None else Some(result) 405 | } 406 | } --------------------------------------------------------------------------------