diff --git a/Dsl/src/main/scala/com/thoughtworks/dsl/Dsl.scala b/Dsl/src/main/scala/com/thoughtworks/dsl/Dsl.scala index 39ed2c1c3..8abd4f551 100644 --- a/Dsl/src/main/scala/com/thoughtworks/dsl/Dsl.scala +++ b/Dsl/src/main/scala/com/thoughtworks/dsl/Dsl.scala @@ -6,8 +6,9 @@ import com.thoughtworks.enableMembersIf import scala.annotation._ import scala.collection._ import scala.collection.mutable.Builder -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds +import scala.util.control.Exception.Catcher import scala.util.{Failure, Success, Try} import scala.util.control.{NonFatal, TailCalls} import scala.util.control.TailCalls.TailRec @@ -262,4 +263,262 @@ object Dsl extends LowPriorityDsl0 { } + private def catchNativeException[A](futureContinuation: Future[A] !! A): Future[A] = { + try { + futureContinuation(Future.successful) + } catch { + case NonFatal(e) => + Future.failed(e) + } + } + + /** The type class to support `try` ... `catch` ... `finally` expression for `OutputDomain`. + * + * !-notation is allowed by default for `? !! Throwable` and [[scala.concurrent.Future Future]] domains, + * with the help of this type class. + */ + @implicitNotFound( + "The `try` ... `catch` ... `finally` expression cannot contain !-notation inside a function that returns ${OuterDomain}.") + trait TryCatchFinally[Value, OuterDomain, BlockDomain, FinalizerDomain] { + def tryCatchFinally(block: BlockDomain !! Value, + catcher: Catcher[BlockDomain !! Value], + finalizer: FinalizerDomain !! Unit, + outerSuccessHandler: Value => OuterDomain): OuterDomain + } + + object TryCatchFinally { + implicit final class Ops[Value, OuterDomain, BlockDomain, FinalizerDomain]( + outerSuccessHandler: Value => OuterDomain)( + implicit typeClass: TryCatchFinally[Value, OuterDomain, BlockDomain, FinalizerDomain] + ) { + def apply(block: BlockDomain !! Value, + catcher: Catcher[BlockDomain !! Value], + finalizer: FinalizerDomain !! Unit): OuterDomain = + typeClass.tryCatchFinally(block, catcher, finalizer, outerSuccessHandler) + } + + implicit def fromTryCatchTryFinally[Value, OuterDomain, BlockDomain, FinalizerDomain]( + implicit tryFinally: TryFinally[Value, OuterDomain, BlockDomain, FinalizerDomain], + tryCatch: TryCatch[Value, BlockDomain, BlockDomain] + ): TryCatchFinally[Value, OuterDomain, BlockDomain, FinalizerDomain] = { + (block: BlockDomain !! Value, + catcher: Catcher[BlockDomain !! Value], + finalizer: FinalizerDomain !! Unit, + outerSuccessHandler: Value => OuterDomain) => + tryFinally.tryFinally({ + tryCatch.tryCatch(block, catcher, _) + }, finalizer, outerSuccessHandler) + } + } + + @implicitNotFound( + "The `try` ... `catch` expression cannot contain !-notation inside a function that returns ${OuterDomain}.") + trait TryCatch[Value, OuterDomain, BlockDomain] { + def tryCatch(block: BlockDomain !! Value, + catcher: Catcher[BlockDomain !! Value], + outerSuccessHandler: Value => OuterDomain): OuterDomain + } + + private[dsl] trait LowPriorityTryCatch { + implicit def liftFunction1TryCatch[Value, OuterDomain, BlockDomain, State]( + implicit restTryCatch: TryCatch[Value, OuterDomain, BlockDomain]) + : TryCatch[Value, State => OuterDomain, State => BlockDomain] = { + (block: (State => BlockDomain) !! Value, + catcher: Catcher[(State => BlockDomain) !! Value], + outerSuccessHandler: Value => State => OuterDomain) => (state: State) => + def withState(blockContinuation: (State => BlockDomain) !! Value) = { blockHandler: (Value => BlockDomain) => + blockContinuation { value: Value => (state: State) => + blockHandler(value) + }(state) + } + + restTryCatch.tryCatch(withState(block), catcher.andThen(withState _), outerSuccessHandler(_)(state)) + } + } + + object TryCatch extends LowPriorityTryCatch { + + implicit final class Ops[Value, OuterDomain, BlockDomain](outerSuccessHandler: Value => OuterDomain)( + implicit typeClass: TryCatch[Value, OuterDomain, BlockDomain] + ) { + def apply(block: BlockDomain !! Value, catcher: Catcher[BlockDomain !! Value]) = { + typeClass.tryCatch(block, catcher, outerSuccessHandler) + } + } + + implicit def futureTryCatch[BlockValue, OuterValue]( + implicit executionContext: ExecutionContext): TryCatch[BlockValue, Future[OuterValue], Future[BlockValue]] = { + (block: Future[BlockValue] !! BlockValue, + catcher: Catcher[Future[BlockValue] !! BlockValue], + outerSuccessHandler: BlockValue => Future[OuterValue]) => + catchNativeException(block) + .recoverWith { + case e: Throwable => + def recover(): Future[BlockValue] = { + (try { + catcher.lift(e) + } catch { + case NonFatal(extractorException) => + return Future.failed(extractorException) + }) match { + case None => + Future.failed(e) + case Some(recovered) => + catchNativeException(recovered) + } + } + recover() + } + .flatMap(outerSuccessHandler) + } + + implicit def throwableContinuationTryCatch[LeftDomain, Value] + : TryCatch[Value, LeftDomain !! Throwable, LeftDomain !! Throwable] = { + + (block: LeftDomain !! Throwable !! Value, + catcher: Catcher[LeftDomain !! Throwable !! Value], + outerSuccessHandler: Value => LeftDomain !! Throwable) => outerFailureHandler => + def innerFailureHandler(e: Throwable): LeftDomain = { + catcher.lift(e) match { + case None => + outerFailureHandler(e) + case Some(recovered) => + @inline + def recoveredHandler(): LeftDomain = { + locally { + try { + recovered(outerSuccessHandler) + } catch { + case NonFatal(nativeThrown) => + return outerFailureHandler(nativeThrown) + } + }(outerFailureHandler) + } + + recoveredHandler() + } + } + + def runBlock(): LeftDomain = { + (try { + block { a => hookedFailureHandler => + @inline + def successHandler(): LeftDomain = { + locally { + try { + outerSuccessHandler(a) + } catch { + case NonFatal(nativeThrown) => + return outerFailureHandler(nativeThrown) + } + }(outerFailureHandler) + } + + successHandler() + } + } catch { + case NonFatal(e) => + return innerFailureHandler(e) + })(innerFailureHandler) + } + runBlock() + } + } + + @implicitNotFound( + "The `try` ... `finally` expression cannot contain !-notation inside a function that returns ${OuterDomain}.") + trait TryFinally[Value, OuterDomain, BlockDomain, FinalizerDomain] { + def tryFinally(block: BlockDomain !! Value, + finalizer: FinalizerDomain !! Unit, + outerSuccessHandler: Value => OuterDomain): OuterDomain + } + + private[dsl] trait LowPriorityTryFinally { + implicit def liftFunction1TryCatch[Value, OuterDomain, BlockDomain, FinalizerDomain, State]( + implicit restTryFinally: TryFinally[Value, OuterDomain, BlockDomain, FinalizerDomain]) + : TryFinally[Value, State => OuterDomain, State => BlockDomain, State => FinalizerDomain] = { + (block: (State => BlockDomain) !! Value, + finalizer: (State => FinalizerDomain) !! Unit, + outerSuccessHandler: Value => State => OuterDomain) => state => + def withState[Domain, Value](blockContinuation: (State => Domain) !! Value) = { blockHandler: (Value => Domain) => + blockContinuation { value: Value => (state: State) => + blockHandler(value) + }(state) + } + + restTryFinally.tryFinally(withState(block), withState(finalizer), outerSuccessHandler(_)(state)) + } + } + + object TryFinally extends LowPriorityTryFinally { + + implicit final class Ops[Value, OuterDomain, BlockDomain, FinalizerDomain] @inline()( + outerSuccessHandler: Value => OuterDomain)( + implicit typeClass: TryFinally[Value, OuterDomain, BlockDomain, FinalizerDomain]) { + @inline + def apply(block: BlockDomain !! Value, finalizer: FinalizerDomain !! Unit): OuterDomain = { + typeClass.tryFinally(block, finalizer, outerSuccessHandler) + } + } + + implicit def futureTryFinally[BlockValue, OuterValue](implicit executionContext: ExecutionContext) + : TryFinally[BlockValue, Future[OuterValue], Future[BlockValue], Future[Unit]] = { + (block: Future[BlockValue] !! BlockValue, + finalizer: Future[Unit] !! Unit, + outerSuccessHandler: BlockValue => Future[OuterValue]) => + @inline + def injectFinalizer[A](f: Unit => Future[A]): Future[A] = { + catchNativeException(finalizer).flatMap(f) + } + + catchNativeException(block) + .recoverWith { + case e: Throwable => + injectFinalizer { _: Unit => + Future.failed(e) + } + } + .flatMap { a => + injectFinalizer { _: Unit => + outerSuccessHandler(a) + } + } + } + + implicit def throwableContinuationTryFinally[LeftDomain, Value] + : TryFinally[Value, LeftDomain !! Throwable, LeftDomain !! Throwable, LeftDomain !! Throwable] = { + (block, finalizer, outerSuccessHandler) => outerFailureHandler => + @inline + def injectFinalizer(finalizerHandler: Unit => LeftDomain !! Throwable): LeftDomain = { + locally { + try { + finalizer(finalizerHandler) + } catch { + case NonFatal(e) => + return outerFailureHandler(e) + } + }(outerFailureHandler) + } + + @inline + def hookedFailureHandler(e: Throwable) = + injectFinalizer { _: Unit => + _(e) + } + + def runBlock(): LeftDomain = { + (try { + block { value => hookedFailureHandler => + injectFinalizer { _: Unit => + outerSuccessHandler(value) + } + } + } catch { + case NonFatal(e) => + return hookedFailureHandler(e) + })(hookedFailureHandler) + } + runBlock() + } + } } diff --git a/build.sbt b/build.sbt index 334100664..9de5b8d96 100644 --- a/build.sbt +++ b/build.sbt @@ -38,7 +38,7 @@ lazy val `keywords-Fork` = scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-BangNotation` in Compile).value}""", scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-ResetEverywhere` in Compile).value}""" ) - .dependsOn(Dsl, `keywords-Shift`, `keywords-Catch`, `keywords-Continue`, `keywords-ForEach`) + .dependsOn(Dsl, `keywords-Shift`, `keywords-Catch` % Optional, `keywords-Continue`, `keywords-ForEach`) lazy val `keywords-ForkJS` = `keywords-Fork`.js lazy val `keywords-ForkJVM` = `keywords-Fork`.jvm @@ -96,7 +96,11 @@ lazy val `keywords-AsynchronousIo` = scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-BangNotation` in Compile).value}""", scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-ResetEverywhere` in Compile).value}""" ) - .dependsOn(`keywords-Shift`, `keywords-Each` % Test, `keywords-Using` % Test, `comprehension` % Test, `domains-task` % Test) + .dependsOn(`keywords-Shift`, + `keywords-Each` % Test, + `keywords-Using` % Test, + `comprehension` % Test, + `domains-task` % Test) lazy val `keywords-AsynchronousIoJS` = `keywords-AsynchronousIo`.js lazy val `keywords-AsynchronousIoJVM` = `keywords-AsynchronousIo`.jvm @@ -111,27 +115,27 @@ lazy val `keywords-Shift` = lazy val `keywords-ShiftJS` = `keywords-Shift`.js lazy val `keywords-ShiftJVM` = `keywords-Shift`.jvm -lazy val `keywords-Using` = +lazy val `keywords-Catch` = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .settings( scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-BangNotation` in Compile).value}""", scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-ResetEverywhere` in Compile).value}""" ) - .dependsOn(Dsl, `keywords-Shift`, `keywords-Catch`) -lazy val `keywords-UsingJS` = `keywords-Using`.js -lazy val `keywords-UsingJVM` = `keywords-Using`.jvm + .dependsOn(Dsl, `keywords-Shift`, `keywords-Yield` % Test) +lazy val `keywords-CatchJS` = `keywords-Catch`.js +lazy val `keywords-CatchJVM` = `keywords-Catch`.jvm -lazy val `keywords-Catch` = +lazy val `keywords-Using` = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .settings( scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-BangNotation` in Compile).value}""", scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-ResetEverywhere` in Compile).value}""" ) - .dependsOn(Dsl, `keywords-Shift`, `keywords-Yield` % Test) -lazy val `keywords-CatchJS` = `keywords-Catch`.js -lazy val `keywords-CatchJVM` = `keywords-Catch`.jvm + .dependsOn(Dsl, `keywords-Shift`, `keywords-Catch` % Optional) +lazy val `keywords-UsingJS` = `keywords-Using`.js +lazy val `keywords-UsingJVM` = `keywords-Using`.jvm lazy val `keywords-Map` = crossProject(JSPlatform, JVMPlatform) @@ -253,7 +257,7 @@ lazy val `domains-scalaz` = scalacOptions += raw"""-Xplugin:${(packageBin in `compilerplugins-ResetEverywhere` in Compile).value}""" ) .dependsOn(Dsl, - `keywords-Catch`, + `keywords-Catch` % Optional, `keywords-Monadic`, `keywords-Return`, `keywords-Shift` % Test, @@ -310,6 +314,27 @@ lazy val `package` = project DslJVM ) +// Replace `keywords-Catch` % Optional to `keywords-Catch` for Scala 2.11 +for (catchProject <- `keywords-Catch`.projects.values.toSeq) yield { + Global / buildDependencies := { + val oldBuildDependencies = (Global / buildDependencies).value + val catchProjectRef = (catchProject / thisProjectRef).value + if (scalaBinaryVersion.value == "2.11") { + internal.BuildDependencies( + oldBuildDependencies.classpath.mapValues(_.map { + case ResolvedClasspathDependency(`catchProjectRef`, Some(Optional.name)) => + ResolvedClasspathDependency(catchProjectRef, None) + case dep => + dep + }), + oldBuildDependencies.aggregate + ) + } else { + oldBuildDependencies + } + } +} + organization in ThisBuild := "com.thoughtworks.dsl" scalacOptions in ThisBuild ++= { diff --git a/compilerplugins-BangNotation/src/main/scala/com/thoughtworks/dsl/compilerplugins/BangNotation.scala b/compilerplugins-BangNotation/src/main/scala/com/thoughtworks/dsl/compilerplugins/BangNotation.scala index 27012e4f5..bbd9e261e 100644 --- a/compilerplugins-BangNotation/src/main/scala/com/thoughtworks/dsl/compilerplugins/BangNotation.scala +++ b/compilerplugins-BangNotation/src/main/scala/com/thoughtworks/dsl/compilerplugins/BangNotation.scala @@ -1,14 +1,12 @@ package com.thoughtworks.dsl package compilerplugins -import com.thoughtworks.dsl.Dsl.{ResetAnnotation, nonTypeConstraintReset, shift} +import com.thoughtworks.dsl.Dsl.{ResetAnnotation, shift} import com.thoughtworks.dsl.compilerplugins.BangNotation.HasReturn -import scala.annotation.tailrec -import scala.tools.nsc.plugins.{Plugin, PluginComponent} -import scala.tools.nsc.transform.Transform +import scala.tools.nsc.plugins.Plugin import scala.tools.nsc.typechecker.ContextMode -import scala.tools.nsc.{Global, Mode, Phase} +import scala.tools.nsc.{Global, Mode} private object BangNotation { sealed trait HasReturn object HasReturn { @@ -157,17 +155,6 @@ final class BangNotation(override val global: Global) extends Plugin { tree.productIterator.exists(hasCpsAttachment) } - private lazy val catchIdent: Tree = { - try { - Ident(rootMirror.staticModule("_root_.com.thoughtworks.dsl.keywords.Catch")) - } catch { - case e: ScalaReflectionException => - abort("""The BangNotation compiler plug-in requires the runtime library `keywords-catch` to enable !-notation in `try` / `catch` / `finally` expressions: - libraryDependencies += "com.thoughtworks.dsl" %% "keywords-catch" % "latest.release" -""") - } - } - private val whileName = currentUnit.freshTermName("while") private val whileDef = { val domainName = currentUnit.freshTypeName("Domain") @@ -390,38 +377,56 @@ final class BangNotation(override val global: Global) extends Plugin { // This CaseDef tree contains some bang notations, and will be translated by enclosing Try or Match tree, not here EmptyTree case Try(block, catches, finalizer) => - val finalizerName = currentUnit.freshTermName("finalizer") val resultName = currentUnit.freshTermName("result") - - q""" - $catchIdent.tryCatch { ($resultName: $tpe) => ${{ - cpsAttachment(finalizer) { finalizerValue => - q""" - ..${notPure(finalizerValue)} - ${continue(q"$resultName")} - """ - } - }}}.apply( - { $finalizerName: ${TypeTree()} => ${cpsAttachment(block) { blockValue => - q"$finalizerName.apply($blockValue)" - }}}, - { - case ..${catches.map { caseDef => - atPos(caseDef.pos) { - treeCopy.CaseDef( - caseDef, - caseDef.pat, - caseDef.guard, - q"""{ $finalizerName: ${TypeTree()} => ${{ - cpsAttachment(caseDef.body) { bodyValue => - q"$finalizerName.apply($bodyValue)" - } - }}}""" - ) - } - }}} - ) - """ + val handlerName = currentUnit.freshTermName("handler") + + def transformBlock() = + q"""{ $handlerName: ${TypeTree()} => ${cpsAttachment(block) { blockValue => + q"$handlerName.apply($blockValue)" + }}}""" + + def transformCatches() = + q"""{case ..${catches.map { caseDef => + atPos(caseDef.pos) { + treeCopy.CaseDef( + caseDef, + caseDef.pat, + caseDef.guard, + q"""{ $handlerName: ${TypeTree()} => ${{ + cpsAttachment(caseDef.body) { bodyValue => + q"$handlerName.apply($bodyValue)" + } + }}}""" + ) + } + }}}""" + + def transformFinalizer() = + q"""{ $handlerName: ${TypeTree()} => + ${cpsAttachment(finalizer) { finalizerValue => + q"$handlerName($finalizerValue: ${typeOf[Unit]})" + }}}""" + + def endTry() = q"""{ ($resultName: $tpe) => ${continue(q"$resultName")}}""" + + finalizer match { + case EmptyTree => + q"""_root_.com.thoughtworks.dsl.Dsl.TryCatch.Ops(${endTry()}).apply( + ${transformBlock()}, + ${transformCatches()} + )""" + case _ if catches.isEmpty => + q"""_root_.com.thoughtworks.dsl.Dsl.TryFinally.Ops(${endTry()}).apply( + ${transformBlock()}, + ${transformFinalizer()} + )""" + case _ => + q"""_root_.com.thoughtworks.dsl.Dsl.TryCatchFinally.Ops(${endTry()}).apply( + ${transformBlock()}, + ${transformCatches()}, + ${transformFinalizer()} + )""" + } case Assign(lhs, rhs) => cpsAttachment(rhs) { rhsValue => continue(treeCopy.Assign(tree, lhs, rhsValue)) diff --git a/domains-scalaz/src/main/scala/com/thoughtworks/dsl/domains/scalaz.scala b/domains-scalaz/src/main/scala/com/thoughtworks/dsl/domains/scalaz.scala index 44945b637..94d415e89 100644 --- a/domains-scalaz/src/main/scala/com/thoughtworks/dsl/domains/scalaz.scala +++ b/domains-scalaz/src/main/scala/com/thoughtworks/dsl/domains/scalaz.scala @@ -2,16 +2,17 @@ package com.thoughtworks.dsl.domains import com.thoughtworks.Extractor._ import com.thoughtworks.dsl.Dsl -import com.thoughtworks.dsl.Dsl.{!!, Keyword} +import com.thoughtworks.dsl.Dsl.!! import scala.language.higherKinds import scala.language.implicitConversions -import _root_.scalaz.{Applicative, Bind, Monad, MonadError, MonadTrans, Unapply} +import _root_.scalaz.{Applicative, Bind, Monad, MonadError, MonadTrans} import com.thoughtworks.dsl.keywords.Catch.CatchDsl -import com.thoughtworks.dsl.keywords.{Catch, Monadic, Return} +import com.thoughtworks.dsl.keywords.{Monadic, Return} +import com.thoughtworks.dsl.Dsl.{TryCatch, TryFinally} import scala.util.control.Exception.Catcher -import scala.util.control.{ControlThrowable, NonFatal} +import scala.util.control.NonFatal /** Contains interpreters to enable [[Dsl.Keyword#unary_$bang !-notation]] * for [[keywords.Monadic Monadic]] and other keywords @@ -128,7 +129,7 @@ object scalaz { protected type MonadThrowable[F[_]] = MonadError[F, Throwable] - implicit def scalazCatchDsl[F[_], A, B](implicit monadError: MonadThrowable[F]): CatchDsl[F[A], F[B], A] = + private[dsl] def scalazCatchDsl[F[_], A, B](implicit monadError: MonadThrowable[F]): CatchDsl[F[A], F[B], A] = new CatchDsl[F[A], F[B], A] { def tryCatch(block: F[A] !! A, catcher: Catcher[F[A] !! A], handler: A => F[B]): F[B] = { import _root_.scalaz.syntax.all._ @@ -143,14 +144,67 @@ object scalaz { } } - implicit def scalazReturnDsl[F[_], A, B](implicit applicative: Applicative[F], - restReturnDsl: Dsl[Return[A], B, Nothing]) = - new Dsl[Return[A], F[B], Nothing] { - def cpsApply(keyword: Return[A], handler: Nothing => F[B]): F[B] = { - applicative.pure(restReturnDsl.cpsApply(keyword, identity)) + @inline private def catchNativeException[F[_], A](continuation: F[A] !! A)( + implicit monadThrowable: MonadThrowable[F]): F[A] = { + try { + continuation(monadThrowable.pure(_)) + } catch { + case NonFatal(e) => + monadThrowable.raiseError(e) + } + } + + implicit def scalazTryFinally[F[_], A, B]( + implicit monadError: MonadThrowable[F]): TryFinally[A, F[B], F[A], F[Unit]] = + new TryFinally[A, F[B], F[A], F[Unit]] { + def tryFinally(block: F[A] !! A, finalizer: F[Unit] !! Unit, outerSuccessHandler: A => F[B]): F[B] = { + @inline + def injectFinalizer[A](f: Unit => F[A]): F[A] = { + monadError.bind(catchNativeException(finalizer))(f) + } + monadError.bind(monadError.handleError(catchNativeException(block)) { e: Throwable => + injectFinalizer { _: Unit => + monadError.raiseError(e) + } + }) { a => + injectFinalizer { _: Unit => + outerSuccessHandler(a) + } + } } } + implicit def scalazTryCatch[F[_], A, B](implicit monadError: MonadThrowable[F]): TryCatch[A, F[B], F[A]] = + new TryCatch[A, F[B], F[A]] { + def tryCatch(block: F[A] !! A, catcher: Catcher[F[A] !! A], outerSuccessHandler: A => F[B]): F[B] = { + import monadError.monadErrorSyntax._ + catchNativeException(block) + .handleError { e => + def recover(): F[A] = { + (try { + catcher.lift(e) + } catch { + case NonFatal(extractorException) => + return monadError.raiseError(extractorException) + }) match { + case None => + monadError.raiseError(e) + case Some(recovered) => + catchNativeException(recovered) + } + } + recover() + } + .flatMap(outerSuccessHandler) + } + } + + implicit def scalazReturnDsl[F[_], A, B](implicit applicative: Applicative[F], + restReturnDsl: Dsl[Return[A], B, Nothing]) = { + (keyword: Return[A], handler: Nothing => F[B]) => + applicative.pure(restReturnDsl.cpsApply(keyword, identity)) + } + implicit def scalazMonadTransformerDsl1[F[_[_], _], H[_], G[_], A, B]( implicit monadTrans: MonadTrans[F], rest: ScalazTransformerDsl[H, G, A, B]): ScalazTransformerDsl[H, F[G, ?], A, B] = diff --git a/domains-task/.jvm/src/test/scala/com/thoughtworks/dsl/domains/RaiiSpec.scala b/domains-task/.jvm/src/test/scala/com/thoughtworks/dsl/domains/RaiiSpec.scala index 5735af552..926b3d92e 100644 --- a/domains-task/.jvm/src/test/scala/com/thoughtworks/dsl/domains/RaiiSpec.scala +++ b/domains-task/.jvm/src/test/scala/com/thoughtworks/dsl/domains/RaiiSpec.scala @@ -2,7 +2,7 @@ package com.thoughtworks.dsl package domains import com.thoughtworks.dsl.Dsl.{!!, Continuation, reset} -import com.thoughtworks.dsl.keywords.{Catch, Shift, Yield} +import com.thoughtworks.dsl.keywords.{Shift, Yield} import org.scalatest.{FreeSpec, Matchers} import com.thoughtworks.dsl.domains.task._ @@ -30,7 +30,8 @@ final class RaiiSpec extends FreeSpec with Matchers { */ def successContinuation[LeftDomain](domain: LeftDomain): (LeftDomain !! Throwable) @reset = Continuation.empty(domain) - def failureContinuation[LeftDomain](throwable: Throwable): (LeftDomain !! Throwable) @reset = Continuation.now(throwable) + def failureContinuation[LeftDomain](throwable: Throwable): (LeftDomain !! Throwable) @reset = + Continuation.now(throwable) "Given a continuation that throws an exception" - { object MyException extends Exception diff --git a/keywords-Catch/src/main/scala/com/thoughtworks/dsl/keywords/Catch.scala b/keywords-Catch/src/main/scala/com/thoughtworks/dsl/keywords/Catch.scala index dbaa93b31..0a0e95eaa 100644 --- a/keywords-Catch/src/main/scala/com/thoughtworks/dsl/keywords/Catch.scala +++ b/keywords-Catch/src/main/scala/com/thoughtworks/dsl/keywords/Catch.scala @@ -13,6 +13,7 @@ import scala.util.control.NonFatal /** * @author 杨博 (Yang Bo) */ +@deprecated("[[keywords.Catch]] will be removed in favor of [[Dsl.TryCatch]].", "Dsl.scala 1.4.0") final case class Catch[Domain, Value](block: Domain !! Value, catcher: Catcher[Domain !! Value]) extends Keyword[Catch[Domain, Value], Value] @@ -47,6 +48,7 @@ private[keywords] trait LowPriorityCatch0 extends LowPriorityCatch1 { this: Catc } } +@deprecated("[[keywords.Catch]] will be removed in favor of [[Dsl.TryCatch]].", "Dsl.scala 1.4.0") object Catch extends LowPriorityCatch0 { type DslCatch[InnerDomain, OuterDomain, Value] = Dsl[Catch[InnerDomain, Value], OuterDomain, Value] diff --git a/keywords-Fork/build.sbt b/keywords-Fork/build.sbt index 88f64dff1..4b467d8c3 100644 --- a/keywords-Fork/build.sbt +++ b/keywords-Fork/build.sbt @@ -17,3 +17,13 @@ libraryDependencies ++= { } libraryDependencies += "com.thoughtworks.enableIf" %% "enableif" % "1.1.7" + +scalacOptions ++= { + import Ordering.Implicits._ + if (VersionNumber(scalaVersion.value).numbers < Seq(2L, 12L)) { + // Enable SAM types for Scala 2.11 + Some("-Xexperimental") + } else { + None + } +} diff --git a/keywords-Fork/src/main/scala/com/thoughtworks/dsl/keywords/Fork.scala b/keywords-Fork/src/main/scala/com/thoughtworks/dsl/keywords/Fork.scala index 31eefb5a7..124d7a23b 100644 --- a/keywords-Fork/src/main/scala/com/thoughtworks/dsl/keywords/Fork.scala +++ b/keywords-Fork/src/main/scala/com/thoughtworks/dsl/keywords/Fork.scala @@ -6,12 +6,14 @@ import java.util.concurrent.atomic.AtomicInteger import com.thoughtworks.dsl.Dsl import com.thoughtworks.dsl.Dsl.{!!, Keyword} import com.thoughtworks.dsl.keywords.Catch.{CatchDsl, DslCatch} +import com.thoughtworks.dsl.Dsl.{TryCatch, TryCatchFinally, TryFinally} import com.thoughtworks.enableMembersIf import scala.collection._ import scala.collection.generic.CanBuildFrom import scala.collection.mutable.Builder import scala.language.implicitConversions +import scala.util.control.NonFatal final case class Fork[Element](elements: Traversable[Element]) extends AnyVal with Keyword[Fork[Element], Element] @@ -101,11 +103,10 @@ object Fork { isTraversableOnce: RightDomain => TraversableOnce[WidenElement], canBuildFrom: Factory[WidenElement, RightDomain], continueDsl: Dsl[Continue, LeftDomain, Nothing], - catchDsl: DslCatch[LeftDomain, LeftDomain, Unit] - ): Dsl[Fork[NarrowElement], LeftDomain !! RightDomain, NarrowElement] = - new Dsl[Fork[NarrowElement], LeftDomain !! RightDomain, NarrowElement] { - def cpsApply(fork: Fork[NarrowElement], - mapper: NarrowElement => LeftDomain !! RightDomain): LeftDomain !! RightDomain = _ { + tryCatchFinally: TryCatchFinally[Unit, LeftDomain, LeftDomain, LeftDomain] + ): Dsl[Fork[NarrowElement], LeftDomain !! RightDomain, NarrowElement] = { + (fork: Fork[NarrowElement], mapper: NarrowElement => LeftDomain !! RightDomain) => + _ { val builder: mutable.Builder[WidenElement, RightDomain] = newBuilder[WidenElement, RightDomain] val exceptionBuilder = Set.newBuilder[Throwable] val counter = new AtomicInteger(1) @@ -118,11 +119,7 @@ object Fork { builder ++= result } } catch { - case MultipleException(throwableSet) => - exceptionBuilder.synchronized[Unit] { - exceptionBuilder ++= throwableSet - } - case e: Throwable => + case NonFatal(e) => exceptionBuilder.synchronized[Unit] { exceptionBuilder += e } @@ -137,6 +134,71 @@ object Fork { } } + val exceptions = exceptionBuilder.result() + if (exceptions.isEmpty) { + builder.result() + } else { + val i = exceptions.iterator + val firstException = i.next() + if (i.hasNext) { + throw MultipleException(exceptions) + } else { + throw firstException + } + } + } + } + + @deprecated("[[keywords.Catch]] will be removed in favor of [[Dsl.TryCatch]].", "Dsl.scala 1.4.0") + private[Fork] def forkContinuationDsl[NarrowElement, LeftDomain, WidenElement, RightDomain]( + implicit eachDsl: Dsl[ForEach[NarrowElement], LeftDomain, NarrowElement], + booleanEachDsl: Dsl[ForEach[Boolean], LeftDomain, Boolean], + isTraversableOnce: RightDomain => TraversableOnce[WidenElement], + canBuildFrom: Factory[WidenElement, RightDomain], + continueDsl: Dsl[Continue, LeftDomain, Nothing], + catchDsl: DslCatch[LeftDomain, LeftDomain, Unit] + ): Dsl[Fork[NarrowElement], LeftDomain !! RightDomain, NarrowElement] = + new Dsl[Fork[NarrowElement], LeftDomain !! RightDomain, NarrowElement] { + def cpsApply(fork: Fork[NarrowElement], + mapper: NarrowElement => LeftDomain !! RightDomain): LeftDomain !! RightDomain = _ { + val builder: mutable.Builder[WidenElement, RightDomain] = newBuilder[WidenElement, RightDomain] + val exceptionBuilder = Set.newBuilder[Throwable] + val counter = new AtomicInteger(1) + if (!ForEach(Seq(true, false))) { + val element = !ForEach(fork.elements) + counter.incrementAndGet() + def tryCatch(): LeftDomain !! Unit = { + Catch + .tryCatch(_) + .apply( + _ { + val result = !Shift(mapper(element)) + builder.synchronized[Unit] { + builder ++= result + } + if (counter.decrementAndGet() > 0) { + !Continue + } + }, { + case NonFatal(e) => + _ { + exceptionBuilder.synchronized[Unit] { + exceptionBuilder += e + } + if (counter.decrementAndGet() > 0) { + !Continue + } + } + } + ) + } + !Shift(tryCatch()) + } else { + if (counter.decrementAndGet() > 0) { + !Continue + } + } + val exceptions = exceptionBuilder.result() if (exceptions.isEmpty) { builder.result() diff --git a/keywords-Using/src/main/scala/com/thoughtworks/dsl/keywords/Using.scala b/keywords-Using/src/main/scala/com/thoughtworks/dsl/keywords/Using.scala index 584b839f1..8063f24b4 100644 --- a/keywords-Using/src/main/scala/com/thoughtworks/dsl/keywords/Using.scala +++ b/keywords-Using/src/main/scala/com/thoughtworks/dsl/keywords/Using.scala @@ -4,6 +4,7 @@ package keywords import com.thoughtworks.dsl.Dsl import com.thoughtworks.dsl.Dsl.{!!, Keyword} import com.thoughtworks.dsl.keywords.Catch.{CatchDsl, DslCatch} +import com.thoughtworks.dsl.Dsl.TryFinally import scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions @@ -60,9 +61,31 @@ object Using { def apply[R <: AutoCloseable](r: => R)( implicit dummyImplicit: DummyImplicit = DummyImplicit.dummyImplicit): Using[R] = new Using(r _) - implicit def throwableContinuationUsingDsl[Domain, Value, R <: AutoCloseable]( + @deprecated("[[keywords.Catch]] will be removed in favor of [[Dsl.TryCatch]].", "Dsl.scala 1.4.0") + private[Using] def throwableContinuationUsingDsl[Domain, Value, R <: AutoCloseable]( implicit catchDsl: DslCatch[Domain, Domain, Value], shiftDsl: Dsl[Shift[Domain, Value], Domain, Value] + ) + : Dsl[Using[R], Domain !! Value, R] = { + (keyword: Using[R], handler: R => Domain !! Value) => (outerHandler: Value => Domain) => + val r = keyword.open() + Catch + .tryCatch { value: Value => + r.close() + outerHandler(value) + } + .apply(Shift(handler(r)).cpsApply(_), { + case NonFatal(e) => + r.close() + _ { + throw e + } + }) + } + + implicit def continuationUsingDsl[Domain, Value, R <: AutoCloseable]( + implicit tryFinally: TryFinally[Value, Domain, Domain, Domain], + shiftDsl: Dsl[Shift[Domain, Value], Domain, Value] ): Dsl[Using[R], Domain !! Value, R] = { (keyword: Using[R], handler: R => Domain !! Value) => _ { val r = keyword.open() @@ -74,7 +97,7 @@ object Using { } } - @deprecated("Use Dsl[Catch[...], ...] as implicit parameters instead of CatchDsl[...]", "Dsl.scala 1.2.0") + @deprecated("[[keywords.Catch]] will be removed in favor of [[Dsl.TryCatch]].", "Dsl.scala 1.2.0") private[Using] def throwableContinuationUsingDsl[Domain, Value, R <: AutoCloseable]( implicit catchDsl: CatchDsl[Domain, Domain, Value], shiftDsl: Dsl[Shift[Domain, Value], Domain, Value] diff --git a/package/src/main/scala/com/thoughtworks/dsl/package.scala b/package/src/main/scala/com/thoughtworks/dsl/package.scala index 1c6b8b804..a602cd442 100644 --- a/package/src/main/scala/com/thoughtworks/dsl/package.scala +++ b/package/src/main/scala/com/thoughtworks/dsl/package.scala @@ -332,6 +332,38 @@ package com.thoughtworks * stream should be(Stream("line1", "line2", "line3")) * isClosed should be(true) * }}} + * + * @example `try` / `catch` / `finally` expressions are also supported in functions that return `Stream[String] !! Throwable`, + * because of the [[scala.Throwable]] part in the signature. + * + * {{{ + * import com.thoughtworks.dsl.Dsl.!!, keywords._ + * var finallyBlockInvoked = 0 + * class MyException extends Exception + * def f: Stream[String] !! Throwable = { + * while (true) { + * try { + * !new Yield("yield value") + * if (true) { + * throw new MyException + * } + * } catch { + * case e: RuntimeException => + * throw new AssertionError("Should not catch an RuntimeException", e) + * } finally { + * finallyBlockInvoked += 1 + * } + * } + * throw new AssertionError("Unreachable code") + * } + * + * val result = f { e => + * e should be(a[MyException]) + * Stream.empty + * } + * result should be(Stream("yield value")) + * finallyBlockInvoked should be(1) + * }}} * @example If you don't need to collaborate to [[scala.Stream Stream]] or other domains, * you can use `TailRec[Unit] !! Throwable !! A` * or the alias [[domains.task.Task]] as the return type, @@ -397,7 +429,7 @@ package com.thoughtworks * {{{ * import com.thoughtworks.dsl.domains.task.Task.blockingAwait * - * val url = new URL("http://localhost:4001/ping") + * val url = new URL("http://localhost:8085/ping") * val fileContent = blockingAwait(httpClient(url)) * fileContent should startWith("HTTP/1.1 200 OK") * }}} @@ -413,8 +445,8 @@ package com.thoughtworks * import com.thoughtworks.dsl.keywords.Fork * import com.thoughtworks.dsl.keywords.Return * val Urls = Seq( - * new URL("http://localhost:4001/ping"), - * new URL("http://localhost:4001/pong") + * new URL("http://localhost:8085/ping"), + * new URL("http://localhost:8085/pong") * ) * def parallelTask: Task[Seq[String]] = { * val url = !Fork(Urls) diff --git a/package/src/test/scala/com/thoughtworks/dsl/MockPingPongServer.scala b/package/src/test/scala/com/thoughtworks/dsl/MockPingPongServer.scala index 4737b427a..21875f96e 100644 --- a/package/src/test/scala/com/thoughtworks/dsl/MockPingPongServer.scala +++ b/package/src/test/scala/com/thoughtworks/dsl/MockPingPongServer.scala @@ -27,7 +27,7 @@ trait MockPingPongServer extends BeforeAndAfterAll { this: Suite => complete("PONG!") } } - concurrent.Await.result(akka.http.scaladsl.Http().bindAndHandle(route, "localhost", 4001), Duration.Inf) + concurrent.Await.result(akka.http.scaladsl.Http().bindAndHandle(route, "localhost", 8085), Duration.Inf) } override protected def afterAll(): Unit = {