Packages

  • package root
    Definition Classes
    root
  • package com
    Definition Classes
    root
  • package thoughtworks
    Definition Classes
    com
  • package dsl

    This project, Dsl.scala, is a framework to create embedded Domain-Specific Languages.

    This project, Dsl.scala, is a framework to create embedded Domain-Specific Languages.

    DSLs written in Dsl.scala are collaborative with others DSLs and Scala control flows. DSL users can create functions that contains interleaved DSLs implemented by different vendors, along with ordinary Scala control flows.

    We also provide some built-in DSLs for asynchronous programming, collection manipulation, and adapters to scalaz.Monad or cats.Monad. Those built-in DSLs can be used as a replacement of for comprehension, scala-continuations, scala-async, Monadless, effectful and ThoughtWorks Each.

    Introduction

    Reinventing control flow in DSL

    Embedded DSLs usually consist of a set of domain-specific keywords, which can be embedded in the their hosting languages.

    Ideally, a domain-specific keyword should be an optional extension, which can be present everywhere in the ordinary control flow of the hosting language. However, previous embedded DSLs usually badly interoperate with hosting language control flow. Instead, they reinvent control flow in their own DSL.

    For example, the akka provides a DSL to create finite-state machines, which consists of some domain-specific keywords like when, goto and stay. Unfortunately, you cannot embedded those keywords into your ordinary if / while / try control flows, because Akka's DSL is required to be split into small closures, preventing ordinary control flows from crossing the boundary of those closures.

    TensorFlow's control flow operations and Caolan's async library are examples of reinventing control flow in languages other than Scala.

    Monad: the generic interface of control flow

    It's too trivial to reinvent the whole set of control flows for each DSL. A simpler approach is only implementing a minimal interface required for control flows for each domain, while the syntax of other control flow operations are derived from the interface, shared between different domains.

    Since computation can be represented as monads, some libraries use monad as the interface of control flow, including scalaz.Monad, cats.Monad and com.twitter.algebird.Monad. A DSL author only have to implement two abstract method in scalaz.Monad, and all the derived control flow operations like scalaz.syntax.MonadOps.whileM, scalaz.syntax.BindOps.ifM are available. In addition, those monadic data type can be created and composed from Scala's built-in for comprehension.

    For example, you can use the same syntax or for comprehension to create random value generators and data-binding expressions, as long as there are Monad instances for org.scalacheck.Gen and com.thoughtworks.binding.Binding respectively.

    Although the effort of creating a DSL is minimized with the help of monads, the syntax is still unsatisfactory. Methods in MonadOps still seem like a duplicate of ordinary control flow, and for comprehension supports only a limited set of functionality in comparison to ordinary control flows. if / while / try and other block expressions cannot appear in the enumerator clause of for comprehension.

    Enabling ordinary control flows in DSL via macros

    An idea to avoid inconsistency between domain-specific control flow and ordinary control flow is converting ordinary control flow to domain-specific control flow at compiler time.

    For example, scala.async provides a macro to generate asynchronous control flow. The users just wrap normal synchronous code in a async block, and it runs asynchronously.

    This approach can be generalized to any monadic data types. ThoughtWorks Each, Monadless and effectful are macros that convert ordinary control flow to monadic control flow.

    For example, with the help of ThoughtWorks Each, Binding.scala is used to create reactive HTML templating from ordinary Scala control flow.

    Delimited continuations

    Another generic interface of control flow is continuation, which is known as the mother of all monads, where control flows in specific domain can be supported by specific final result types of continuations.

    scala-continuations and stateless-future are two delimited continuation implementations. Both projects can convert ordinary control flow to continuation-passing style closure chains at compiler time.

    For example, stateless-future-akka, based on stateless-future, provides a special final result type for akka actors. Unlike akka.actor.FSM's inconsistent control flows, users can create complex finite-state machines from simple ordinary control flows along with stateless-future-akka's domain-specific keyword nextMessage.

    Collaborative DSLs

    All the above approaches lack of the ability to collaborate with other DSLs. Each of the above DSLs can be only exclusively enabled in a code block. For example, scala-continuations enables calls to @cps method in scala.util.continuations.reset blocks, and ThoughtWorks Each enables the magic each method for scalaz.Monad in com.thoughtworks.each.Monadic.monadic blocks. It is impossible to enable both DSLs in one function.

    This Dsl.scala project resolves this problem.

    We also provide adapters to all the above kinds of DSLs. Instead of switching different DSLs between different functions, DSL users can use multiple DSLs together in one function, by simply adding our Scala compiler plug-in.

    Definition Classes
    thoughtworks
    Examples:
    1. Suppose you want to create an Xorshift random number generator. The generated numbers should be stored in a lazily evaluated infinite Stream, which can be implemented as a recursive function that produce the next random number in each iteration, with the help of our built-in domain-specific keyword Yield.

      import com.thoughtworks.dsl.Dsl.reset
      import com.thoughtworks.dsl.keywords.Yield
      
      def xorshiftRandomGenerator(seed: Int): Stream[Int] = {
        val tmp1 = seed ^ (seed << 13)
        val tmp2 = tmp1 ^ (tmp1 >>> 17)
        val tmp3 = tmp2 ^ (tmp2 << 5)
        !Yield(tmp3)
        xorshiftRandomGenerator(tmp3)
      }: @reset
      
      val myGenerator = xorshiftRandomGenerator(seed = 123)
      
      myGenerator(0) should be(31682556)
      myGenerator(1) should be(-276305998)
      myGenerator(2) should be(2101636938)

      Yield is an keyword to produce a value for a lazily evaluated Stream. That is to say, Stream is the domain where the DSL Yield can be used, which was interpreted like the yield keyword in C#, JavaScript or Python. Note that the body of xorshiftRandomGenerator is annotated as @reset, which enables the !-notation in the code block. Alternatively, you can also use the ResetEverywhere compiler plug-in, which enable !-notation for every methods and functions.

    2. ,
    3. Yield and Stream can be also used for logging. Suppose you have a function to parse an JSON file, you can append log records to a Stream during parsing.

      import com.thoughtworks.dsl.keywords.Yield
      import com.thoughtworks.dsl.Dsl.!!
      import scala.util.parsing.json._
      def parseAndLog1(jsonContent: String, defaultValue: JSONType): Stream[String] !! JSONType = { (callback: JSONType => Stream[String]) =>
        !Yield(s"I am going to parse the JSON text $jsonContent...")
        JSON.parseRaw(jsonContent) match {
          case Some(json) =>
            !Yield(s"Succeeded to parse $jsonContent")
            callback(json)
          case None =>
            !Yield(s"Failed to parse $jsonContent")
            callback(defaultValue)
        }
      }

      Since the function produces both a JSONType and a Stream of logs, the return type is now Stream[String] !! JSONType, where !! is (JSONType => Stream[String]) => Stream[String], an alias of continuation-passing style function, indicating it produces both a JSONType and a Stream of logs.

      val logs = parseAndLog1(""" { "key": "value" } """, JSONArray(Nil)) { json =>
        json should be(JSONObject(Map("key" -> "value")))
        Stream("done")
      }
      
      logs should be(Stream("I am going to parse the JSON text  { \"key\": \"value\" } ...",
                            "Succeeded to parse  { \"key\": \"value\" } ",
                            "done"))
    4. ,
    5. The closure in the previous example can be simplified with the help of Scala's placeholder syntax:

      import com.thoughtworks.dsl.keywords.Yield
      import com.thoughtworks.dsl.Dsl.!!
      import scala.util.parsing.json._
      def parseAndLog2(jsonContent: String, defaultValue: JSONType): Stream[String] !! JSONType = _ {
        !Yield(s"I am going to parse the JSON text $jsonContent...")
        JSON.parseRaw(jsonContent) match {
          case Some(json) =>
            !Yield(s"Succeeded to parse $jsonContent")
            json
          case None =>
            !Yield(s"Failed to parse $jsonContent")
            defaultValue
        }
      }
      
      val logs = parseAndLog2(""" { "key": "value" } """, JSONArray(Nil)) { json =>
        json should be(JSONObject(Map("key" -> "value")))
        Stream("done")
      }
      
      logs should be(Stream("I am going to parse the JSON text  { \"key\": \"value\" } ...",
                            "Succeeded to parse  { \"key\": \"value\" } ",
                            "done"))

      Note that parseAndLog2 is equivelent to parseAndLog1. The code block after underscore is still inside a function whose return type is Stream[String].

    6. ,
    7. Instead of manually create the continuation-passing style function, you can also create the function from a !! block.

      import com.thoughtworks.dsl.keywords.Yield
      import com.thoughtworks.dsl.Dsl.!!
      import scala.util.parsing.json._
      def parseAndLog3(jsonContent: String, defaultValue: JSONType): Stream[String] !! JSONType = !! {
        !Yield(s"I am going to parse the JSON text $jsonContent...")
        JSON.parseRaw(jsonContent) match {
          case Some(json) =>
            !Yield(s"Succeeded to parse $jsonContent")
            json
          case None =>
            !Yield(s"Failed to parse $jsonContent")
            defaultValue
        }
      }
      
      val logs = parseAndLog3(""" { "key": "value" } """, JSONArray(Nil)) { json =>
        json should be(JSONObject(Map("key" -> "value")))
        Stream("done")
      }
      
      logs should be(Stream("I am going to parse the JSON text  { \"key\": \"value\" } ...",
                            "Succeeded to parse  { \"key\": \"value\" } ",
                            "done"))

      Unlike the parseAndLog2 example, The code inside a !! block is not in an anonymous function. Instead, they are directly inside parseAndLog3, whose return type is Stream[String] !! JSONType. That is to say, the domain of those Yield keywords in parseAndLog3 is not Stream[String] any more, the domain is Stream[String] !! JSONType now, which supports more keywords, which you will learnt from the next examples, than the Stream[String] domain.

    8. ,
    9. !!, or Continuation, is the preferred approach to enable multiple domains in one function. For example, you can create a function that lazily read each line of a BufferedReader to a Stream, automatically close the BufferedReader after reading the last line, and finally return the total number of lines in the Stream[String] !! Throwable !! Int domain.

      import com.thoughtworks.dsl.Dsl.!!
      import com.thoughtworks.dsl.keywords.Using
      import com.thoughtworks.dsl.keywords.Yield
      import com.thoughtworks.dsl.keywords.Shift._
      import java.io._
      
      def readerToStream(createReader: () => BufferedReader): Stream[String] !! Throwable !! Int = !! {
        val reader = !Using(createReader())
      
        def loop(lineNumber: Int): Stream[String] !! Throwable !! Int = _ {
          reader.readLine() match {
            case null =>
              lineNumber
            case line =>
              !Yield(line)
              !loop(lineNumber + 1)
          }
        }
      
        !loop(0)
      }

      !loop(0) is a shortcut of !Shift(loop(0)), because there is an implicit conversion from Stream[String] !! Throwable !! Int to a keywords.Shift case class, which is similar to the await keyword in JavaScript, Python or C#. A type like A !! B !! C means a domain-specific value of type C in the domain of A and B. When B is Throwable, the keywords.Using is available, which will close a resource when exiting the current function.

      import scala.util.Success
      
      var isClosed = false
      def createReader() = {
        new BufferedReader(new StringReader("line1\nline2\nline3")) {
          override def close() = {
            isClosed = true
          }
        }
      }
      
      val stream = readerToStream(createReader _) { numberOfLines: Int =>
        numberOfLines should be(3)
      
        Function.const(Stream.empty)(_)
      } { e: Throwable =>
        throw new AssertionError("Unexpected exception during readerToStream", e)
      }
      
      isClosed should be(false)
      stream should be(Stream("line1", "line2", "line3"))
      isClosed should be(true)
    10. ,
    11. 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)
    12. ,
    13. If you don't need to collaborate to Stream or other domains, you can use TailRec[Unit] !! Throwable !! A or the alias domains.task.Task as the return type, which can be used as an asynchronous task that support RAII, as a higher-performance replacement of scala.concurrent.Future, scalaz.concurrent.Task or monix.eval.Task. Also, there are some keywords in keywords.AsynchronousIo to asynchronously perform Java NIO.2 IO operations in a domains.task.Task domain. For example, you can implement an HTTP client from those keywords.

      import com.thoughtworks.dsl.domains.task._
      import com.thoughtworks.dsl.keywords._
      import com.thoughtworks.dsl.keywords.Shift.implicitShift
      import com.thoughtworks.dsl.keywords.AsynchronousIo._
      import java.io._
      import java.net._
      import java.nio._, channels._
      
      def readAll(channel: AsynchronousByteChannel, destination: ByteBuffer): Task[Unit] = _ {
        if (destination.remaining > 0) {
          val numberOfBytesRead: Int = !Read(channel, destination)
          numberOfBytesRead match {
            case -1 =>
            case _  => !readAll(channel, destination)
          }
        } else {
          throw new IOException("The response is too big to read.")
        }
      }
      
      def writeAll[Domain](channel: AsynchronousByteChannel, destination: ByteBuffer): Task[Unit] = _ {
        while (destination.remaining > 0) {
          !Write(channel, destination)
        }
      }
      
      def httpClient(url: URL): Task[String] = _ {
        val socket = AsynchronousSocketChannel.open()
        try {
          val port = if (url.getPort == -1) 80 else url.getPort
          val address = new InetSocketAddress(url.getHost, port)
          !AsynchronousIo.Connect(socket, address)
          val request = ByteBuffer.wrap(s"GET ${url.getPath} HTTP/1.1\r\nHost:${url.getHost}\r\nConnection:Close\r\n\r\n".getBytes)
          !writeAll(socket, request)
          val response = ByteBuffer.allocate(100000)
          !readAll(socket, response)
          response.flip()
          io.Codec.UTF8.decoder.decode(response).toString
        } finally {
          socket.close()
        }
      }

      The usage of Task is similar to previous examples. You can check the result or exception in asynchronous handlers. But we also provides blockingAwait and some other utilities at domains.task.Task.

      import com.thoughtworks.dsl.domains.task.Task.blockingAwait
      
      val url = new URL("http://localhost:8085/ping")
      val fileContent = blockingAwait(httpClient(url))
      fileContent should startWith("HTTP/1.1 200 OK")

      Another useful keyword for asynchronous programming is Fork, which duplicate the current control flow, and the child control flows are executed in parallel, similar to the POSIX fork system call. Fork should be used inside a com.thoughtworks.dsl.domains.task.Task#join block, which collects the result of each forked control flow.

      import com.thoughtworks.dsl.keywords.Fork
      import com.thoughtworks.dsl.keywords.Return
      val Urls = Seq(
        new URL("http://localhost:8085/ping"),
        new URL("http://localhost:8085/pong")
      )
      def parallelTask: Task[Seq[String]] = {
        val url = !Fork(Urls)
        !Return(!httpClient(url))
      }
      
      inside(blockingAwait(parallelTask)) {
        case Seq(fileContent0, fileContent1) =>
          fileContent0 should startWith("HTTP/1.1 200 OK")
          fileContent1 should startWith("HTTP/1.1 200 OK")
      }
    14. ,
    15. The built-in keywords.Monadic can be used as an adaptor to scalaz.Monad and scalaz.MonadTrans, to create monadic code from imperative syntax, similar to the !-notation in Idris. For example, suppose you are creating a program that counts lines of code under a directory. You want to store the result in a Stream of line count of each file.

      import java.io.File
      import com.thoughtworks.dsl.keywords.Monadic
      import com.thoughtworks.dsl.domains.scalaz._
      import scalaz.std.stream._
      
      def countMonadic(file: File): Stream[Int] = Stream {
        if (file.isDirectory) {
          file.listFiles() match {
            case null =>
              // Unable to open `file`
              !Monadic(Stream.empty[Int])
            case children =>
              // Import this implicit conversion to omit the Monadic keyword
              import com.thoughtworks.dsl.keywords.Monadic.implicitMonadic
      
              val child: File = !children.toStream
              !countMonadic(child)
          }
        } else {
          scala.io.Source.fromFile(file).getLines.size
        }
      }
      
      
      val countCurrentSourceFile = countMonadic(new File(sourcecode.File()))
      
      inside(countCurrentSourceFile) {
        case Stream(lineCount) =>
          lineCount should be > 0
      }
    16. ,
    17. The previous code requires a toStream conversion on children, because children's type Array[File] does not fit the F type parameter in scalaz.Monad.bind. The conversion can be avoided if using CanBuildFrom type class instead of monads. We provide a Each keyword to extract each element in a Scala collection. The behavior is similar to monad, except the collection type can vary. For example, you can extract each element from an Array, even when the return type (or the domain) is a Stream.

      import java.io.File
      import com.thoughtworks.dsl.keywords.Monadic, Monadic.implicitMonadic
      import com.thoughtworks.dsl.keywords.Each
      import com.thoughtworks.dsl.domains.scalaz._
      import scalaz.std.stream._
      
      def countEach(file: File): Stream[Int] = Stream {
        if (file.isDirectory) {
          file.listFiles() match {
            case null =>
              // Unable to open `file`
              !Stream.empty[Int]
            case children =>
              val child: File = !Each(children)
              !countEach(child)
          }
        } else {
          scala.io.Source.fromFile(file).getLines.size
        }
      }
      
      
      val countCurrentSourceFile = countEach(new File(sourcecode.File()))
      
      inside(countCurrentSourceFile) {
        case Stream(lineCount) =>
          lineCount should be > 0
      }

      Unlike Haskell's do-notation or Idris's !-notation, Dsl.scala allows non-monadic keywords like Each works along with monads.

    18. ,
    19. Dsl.scala also supports scalaz.MonadTrans. Considering the line counter implemented in previous example may be failed for some files, due to permission issue or other IO problem, you can use scalaz.OptionT monad transformer to mark those failed file as a None.

      import scalaz._
      import java.io.File
      import com.thoughtworks.dsl.keywords.Monadic, Monadic.implicitMonadic
      import com.thoughtworks.dsl.domains.scalaz._
      import scalaz.std.stream._
      
      def countLift(file: File): OptionT[Stream, Int] = OptionT.some {
        if (file.isDirectory) {
          file.listFiles() match {
            case null =>
              // Unable to open `file`
              !OptionT.none[Stream, Int]
            case children =>
              val child: File = !Stream(children: _*)
              !countLift(child)
          }
        } else {
          scala.io.Source.fromFile(file).getLines.size
        }
      }
      
      
      val countCurrentSourceFile = countLift(new File(sourcecode.File())).run
      
      inside(countCurrentSourceFile) {
        case Stream(Some(lineCount)) =>
          lineCount should be > 0
      }

      Note that our keywords are adaptive to the domain it belongs to. Thus, instead of explicit !Monadic(OptionT.optionTMonadTrans.liftM(Stream(children: _*))), you can simply have !Stream(children: _*). The implicit lifting feature looks like Idris's effect monads, though the mechanisms is different from implicit lift in Idris.

    See also

    Dsl for the guideline to create your custom DSL.

    domains.scalaz for using !-notation with scalaz.

    domains.cats for using !-notation with cats.

  • package compilerplugins
    Definition Classes
    dsl
  • package domains
    Definition Classes
    dsl
  • package keywords

    Contains built-in domain-specific Keywords and their corresponding interpreters.

    Contains built-in domain-specific Keywords and their corresponding interpreters.

    Definition Classes
    dsl
  • AsynchronousIo
  • Await
  • Catch
  • FlatMap
  • ForEach
  • Get
  • Map
  • Monadic
  • NoneSafe
  • NullSafe
  • Put
  • Return
  • Shift
  • Using
  • WithFilter

package keywords

Contains built-in domain-specific Keywords and their corresponding interpreters.

Source
package.scala
Linear Supertypes
Ordering
  1. Alphabetic
  2. By Inheritance
Inherited
  1. keywords
  2. AnyRef
  3. Any
  1. Hide All
  2. Show All
Visibility
  1. Public
  2. Protected

Type Members

  1. trait AsynchronousIo[Value] extends Keyword[AsynchronousIo[Value], Value]

    The base keyword to perform asynchronous IO in domains.task.Tasks.

    The base keyword to perform asynchronous IO in domains.task.Tasks.

    Example:
    1. The following readAll is a Task to read file content with the help of AsynchronousIo.ReadFile

      import java.nio._, file._, channels._
      import com.thoughtworks.dsl.domains.task.Task
      import com.thoughtworks.dsl.keywords._
      import com.thoughtworks.dsl.keywords.Shift._
      import com.thoughtworks.dsl.keywords.AsynchronousIo.ReadFile
      import scala.collection.mutable.ArrayBuffer
      import scala.io.Codec
      def readAll(channel: AsynchronousFileChannel, temporaryBufferSize: Int = 4096): Task[ArrayBuffer[CharBuffer]] = Task {
        val charBuffers = ArrayBuffer.empty[CharBuffer]
        val decoder = Codec.UTF8.decoder
        val byteBuffer = ByteBuffer.allocate(temporaryBufferSize)
        var position: Long = 0L
        while (!ReadFile(channel, byteBuffer, position) != -1) {
          position += byteBuffer.position()
          byteBuffer.flip()
          charBuffers += decoder.decode(byteBuffer)
          byteBuffer.clear()
        }
        charBuffers
      }

      Tasks created from !-notation can be used in for-comprehension, and other keywords can be used together in the same for block. For example, the following cat function contains a single for block to concatenate file contents. It asynchronously iterates elements Seq, ArrayBuffer and String with the help of keywords.Each, managed native resources with the help of keywords.Using, performs previously created readAll task with the help of keywords.Shift, and finally converts the return type as a Task[Vector[Char]].

      import com.thoughtworks.dsl.comprehension._
      import com.thoughtworks.dsl.keywords._
      import com.thoughtworks.dsl.keywords.Shift._
      import com.thoughtworks.dsl.domains.task.Task
      import java.net.URL
      def cat(paths: Path*) = {
        for {
          path <- Each(paths)
          channel <- Using(AsynchronousFileChannel.open(path))
          charBuffers <- readAll(channel)
          charBuffer <- Each(charBuffers)
          char <- Each(charBuffer.toString)
        } yield char
      }.as[Task[Vector[Char]]]

      Then the cat function is used to concatenate files from this project, as shown below:

      Task.toFuture(Task {
        (!cat(Paths.get(".sbtopts"), Paths.get(".scalafmt.conf"))).mkString should be(
          "-J-XX:MaxMetaspaceSize=512M\n-J-Xmx5G\n-J-Xss6M\nversion = \"1.5.1\"\nmaxColumn = 120"
        )
      })
  2. final case class Await[Value](future: Future[Value]) extends AnyVal with Keyword[Await[Value], Value] with Product with Serializable

    Await is a Keyword to extract value from a scala.concurrent.Future.

    Await is a Keyword to extract value from a scala.concurrent.Future.

    This keyword is available in functions whose return types are Future, domains.task.Task, or any exception aware continuations as (_ !! Throwable !! _).

    Author:

    杨博 (Yang Bo)

    Examples:
    1. Given a Future:

      import scala.concurrent.Future
      val myFuture40 = Future {
        40
      }

      You can Await the Future in another Future

      def myFuture42 = Future {
        !Await(myFuture40) + 2
      }

      A Future can be converted to a domains.task.Task with the help of Await.

      import com.thoughtworks.dsl.domains.task.Task
      import com.thoughtworks.dsl.keywords.Await
      val myTask = Task {
        !Await(myFuture42)
      }

      Then a domains.task.Task can be converted back to a scala.concurrent.Future via domains.task.Task.toFuture.

      val myAssertionTask = Task {
        !Shift(myTask) should be(42)
      }
      Task.toFuture(myAssertionTask)
    2. ,
    3. !Await can be used together with try / catch / finally.

      import scala.concurrent.Future
      val buffer = new StringBuffer
      def recoverFuture = Future {
        buffer.append("Oh")
      }
      def exceptionalFuture = Future[StringBuffer] {
        throw new IllegalStateException("No")
      }
      def myFuture = Future {
        try {
          !Await(exceptionalFuture)
        } catch {
          case e: IllegalStateException =>
            !Await(recoverFuture)
            buffer.append(' ')
            buffer.append(e.getMessage)
        } finally {
          buffer.append("!")
        }
      }
      myFuture.map(_.toString should be("Oh No!"))
    4. ,
    5. Other keywords, including Return or Get, can be used together with Await

      import scala.concurrent.Future
      import com.thoughtworks.dsl.keywords.{Get, Return}
      val buffer = new StringBuffer
      def recoverFuture = Future {
        buffer.append("Oh")
      }
      def exceptionalFuture = Future[StringBuffer] {
        throw new IllegalStateException("No")
      }
      def myFuture: Char => Future[StringBuffer] = !Return {
        try {
          !Await(exceptionalFuture)
        } catch {
          case e: IllegalStateException =>
            !Await(recoverFuture)
            buffer.append(!Get[Char])
            buffer.append(e.getMessage)
        } finally {
          buffer.append("!")
        }
      }
      myFuture(' ').map(_.toString should be("Oh No!"))
  3. final case class FlatMap[UpstreamKeyword, UpstreamValue, NestedKeyword, NestedValue](upstream: UpstreamKeyword, flatMapper: (UpstreamValue) => NestedKeyword) extends Keyword[FlatMap[UpstreamKeyword, UpstreamValue, NestedKeyword, NestedValue], NestedValue] with Product with Serializable
  4. final case class ForEach[Element](elements: Traversable[Element]) extends Keyword[ForEach[Element], Element] with Product with Serializable

    Iterates though each element in elements.

  5. final case class Get[S]() extends Keyword[Get[S], S] with Product with Serializable

    Author:

    杨博 (Yang Bo)

    See also

    Put

  6. final case class Map[UpstreamKeyword, UpstreamValue, Value](upstream: UpstreamKeyword, mapper: (UpstreamValue) => Value) extends Keyword[Map[UpstreamKeyword, UpstreamValue, Value], Value] with Product with Serializable
  7. final case class Monadic[F[_], A](fa: F[A]) extends Keyword[Monadic[F, A], A] with Product with Serializable

    A keyword for extracting monadic value from the monadic expression fa.

    A keyword for extracting monadic value from the monadic expression fa.

    To do

    Monadic should be a scala.AnyVal after https://github.com/scala/bug/issues/10595 is resolved.

    See also

    com.thoughtworks.dsl.domains.cats for using this Monadic keyword with cats.Monad.

    com.thoughtworks.dsl.domains.scalaz for using this Monadic keyword with scalaz.Monad.

  8. final case class NoneSafe[A](option: Option[A]) extends AnyVal with Keyword[NoneSafe[A], A] with Product with Serializable
  9. final case class NullSafe[A <: AnyRef](nullable: A @com.thoughtworks.dsl.Dsl.reset) extends AnyVal with Product with Serializable

    NullSafe is a keyword to perform null check.

    NullSafe is a keyword to perform null check.

    Author:

    杨博 (Yang Bo)

    Examples:
    1. You can use ? annotation to represent a nullable value.

      import com.thoughtworks.dsl.keywords.NullSafe._
      
      case class Tree(left: Tree @ ? = null, right: Tree @ ? = null, value: String @ ? = null)
      
      val root: Tree @ ? = Tree(
        left = Tree(
          left = Tree(value = "left-left"),
          right = Tree(value = "left-right")
        ),
        right = Tree(value = "right")
      )

      A normal . is not null safe, when selecting left, right or value on a null value.

      a[NullPointerException] should be thrownBy {
        root.right.left.right.value
      }

      The above code throws an exception because root.right.left is null. The exception can be avoided by using ? on a nullable value:

      root.?.right.?.left.?.right.?.value should be(null)

      The entire expression will be null if one of ? is performed on a null value.


      The boundary of a null safe operator ? is the nearest enclosing expression whose type is annotated as @ ?.

      ("Hello " + ("world " + root.?.right.?.left.?.value)) should be("Hello world null")
      ("Hello " + (("world " + root.?.right.?.left.?.value.?): @ ?)) should be("Hello null")
      (("Hello " + ("world " + root.?.right.?.left.?.value.?)): @ ?) should be(null)
    2. ,
    3. The ? operator usually works with Java libraries that may produce null.

      import com.thoughtworks.dsl.keywords.NullSafe._
      
      val myMap = new java.util.HashMap[String, String]();
      ((myMap.get("key1").? + myMap.get("key2").?): @ ?) should be(null)
    Note

    The ? operator is only available on nullable values. A type is considered as nullable if it is a reference type, no matter it is annotated as @ ? or not.

    import com.thoughtworks.dsl.keywords.NullSafe._
    
    val explicitNullable: String @ ? = null
    ((explicitNullable.? + " Doe") : @ ?) should be(null)
    val implicitNullable: String = null
    ((implicitNullable.? + " Doe") : @ ?) should be(null)

    A type is considered as not nullable if it is a value type.

    val implicitNotNullable: Int = 0
    "(implicitNotNullable.? + 42) : @ ?" shouldNot compile

    Alternatively, a type can be considered as not nullable by explicitly converting it to NotNull.

    val explicitNotNullable: NotNull[String] = NotNull("John")
    """(explicitNotNullable.? + " Doe") : @ ?""" shouldNot compile
    See also

    NoneSafe for similar checks on scala.Options.

  10. final case class Put[S](value: S) extends AnyVal with Keyword[Put[S], Unit] with Product with Serializable

    Put is a Keyword to replace the value of the current context.

    Put is a Keyword to replace the value of the current context.

    Purely functional programming languages usually do not support native first-class mutable variables. In those languages, mutable states can be implemented in state monads.

    Put and Get are the Dsl-based replacements of state monads.

    We use unary function as the domain of mutable state. The parameter of the unary function can be read from the Get keyword, and changed by the Put keyword.

    Author:

    杨博 (Yang Bo)

    Examples:
    1. The following example creates a function that accepts a string parameter and returns the upper-cased last character of the parameter.

      def upperCasedLastCharacter: String => Char = {
        val initialValue = !Get[String]()
        !Put(initialValue.toUpperCase)
      
        val upperCased = !Get[String]()
        Function.const(upperCased.last)
      }

      For example, given a string of foo, the upper-cased last character should be O.

      // Output: O
      upperCasedLastCharacter("foo") should be('O')
    2. ,
    3. Put and Get support multiple states. The following code creates a formatter that Put parts of content into a Vector[Any] of string buffers.

      def formatter: Double => Int => Vector[Any] => String = {
        !Put(!Get[Vector[Any]] :+ "x=")
        !Put(!Get[Vector[Any]] :+ !Get[Double])
        !Put(!Get[Vector[Any]] :+ ",y=")
        !Put(!Get[Vector[Any]] :+ !Get[Int])
      
        !Return((!Get[Vector[Any]]).mkString)
      }
      
      formatter(0.5)(42)(Vector.empty) should be("x=0.5,y=42")
    See also

    Get

  11. final case class Return[ReturnValue](returnValue: ReturnValue) extends AnyVal with Keyword[Return[ReturnValue], Nothing] with Product with Serializable

    A Dsl.Keyword to early return a lifted value from the enclosing function.

    A Dsl.Keyword to early return a lifted value from the enclosing function.

    Author:

    杨博 (Yang Bo)

    Examples:
    1. Suppose you are generating a random integer less than 100, whose first digit and second digit is different. A solution is generating integers in an infinite loop, and Return from the loop when the generated integer conforms with requirements.

      import scala.util.Random
      import scala.util.control.TailCalls
      import scala.util.control.TailCalls.TailRec
      def randomInt(): TailRec[Int] = {
        while (true) {
          val r = Random.nextInt(100)
          if (r % 10 != r / 10) {
            !Return(TailCalls.done(r))
          }
        }
        throw new AssertionError("Unreachable code");
      }
      
      val r = randomInt().result
      r should be < 100
      r % 10 should not be r / 10
    2. ,
    3. Since this Return keyword can automatically lift the return type, TailCalls.done can be omitted.

      import scala.util.Random
      import scala.util.control.TailCalls
      import scala.util.control.TailCalls.TailRec
      def randomInt(): TailRec[Int] = {
        while (true) {
          val r = Random.nextInt(100)
          if (r % 10 != r / 10) {
            !Return(r)
          }
        }
        throw new AssertionError("Unreachable code");
      }
      
      val r = randomInt().result
      r should be < 100
      r % 10 should not be r / 10
  12. final case class Shift[Domain, Value](continuation: !![Domain, Value]) extends AnyVal with Keyword[Shift[Domain, Value], Value] with Product with Serializable

    Author:

    杨博 (Yang Bo)

  13. final case class Using[R <: AutoCloseable](open: () => R) extends AnyVal with Keyword[Using[R], R] with Product with Serializable

    This Using keyword automatically manage resources in scala.concurrent.Future, domains.task.Task, and other asynchrounous domains derived from Future or Task.

    This Using keyword automatically manage resources in scala.concurrent.Future, domains.task.Task, and other asynchrounous domains derived from Future or Task.

    Author:

    杨博 (Yang Bo)

    See also

    dsl for usage of this Using keyword in continuations

  14. final case class WithFilter[UpstreamKeyword, UpstreamValue](upstream: UpstreamKeyword, condition: (UpstreamValue) => Boolean) extends Keyword[WithFilter[UpstreamKeyword, UpstreamValue], UpstreamValue] with Product with Serializable

Deprecated Type Members

  1. final case class Catch[Domain, Value](block: !![Domain, Value], catcher: Catcher[!![Domain, Value]]) extends Keyword[Catch[Domain, Value], Value] with Product with Serializable

    Author:

    杨博 (Yang Bo)

    Annotations
    @deprecated
    Deprecated

    (Since version Dsl.scala 1.4.0) keywords.Catch will be removed in favor of Dsl.TryCatch.

Value Members

  1. object AsynchronousIo
  2. object Await extends Serializable
  3. object FlatMap extends Serializable
  4. object ForEach extends Serializable
  5. object Get extends Serializable
  6. object Map extends Serializable
  7. object Monadic extends Serializable
  8. object NoneSafe extends Serializable
  9. object NullSafe extends Serializable
  10. object Put extends Serializable
  11. object Return extends Serializable
  12. object Shift extends LowPriorityShift0 with Serializable
  13. object Using extends Serializable
  14. object WithFilter extends Serializable

Deprecated Value Members

  1. object Catch extends LowPriorityCatch0 with Serializable
    Annotations
    @deprecated
    Deprecated

    (Since version Dsl.scala 1.4.0) keywords.Catch will be removed in favor of Dsl.TryCatch.

Inherited from AnyRef

Inherited from Any

Ungrouped