├── .git-blame-ignore-revs ├── .github └── workflows │ ├── scala-steward.yml │ └── scala.yml ├── .gitignore ├── .gitpod.yml ├── .scalafmt.conf ├── .vscode └── settings.json ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── each ├── .js │ └── build.sbt ├── .jvm │ └── build.sbt ├── build.sbt.shared └── src │ ├── main │ ├── scala-2.13 │ │ └── com │ │ │ └── thoughtworks │ │ │ └── each │ │ │ └── macrocompat.scala │ └── scala │ │ └── com │ │ └── thoughtworks │ │ └── each │ │ ├── Monadic.scala │ │ └── package.scala │ └── test │ ├── scala-2.11 │ └── com │ │ └── thoughtworks │ │ └── each │ │ ├── ReportingTest.scala │ │ └── TraverseComprehensionTest211.scala │ └── scala │ └── com │ └── thoughtworks │ └── each │ ├── AnnotationTest.scala │ ├── ComprehensionImplicitsTest.scala │ ├── Issue38.scala │ ├── MonadicErrorTest.scala │ ├── MonadicTest.scala │ └── TraverseComprehensionTest.scala ├── project ├── build.properties ├── plugins.sbt ├── plugins.sbt.scala-js.1.x └── sonatypeResolver.sbt ├── secret.sbt └── sonatypeResolver.sbt /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.1.2 2 | fb6cfb8aea15a1b339e3ed69e1e96acd7df4cae6 3 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Launch Scala Steward 5 | 6 | jobs: 7 | scala-steward: 8 | runs-on: ubuntu-22.04 9 | name: Launch Scala Steward 10 | steps: 11 | - name: Launch Scala Steward 12 | uses: scala-steward-org/scala-steward-action@v2 13 | with: 14 | github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} 15 | branches: ${{ github.ref_name }} 16 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "update/**" 7 | tags: 8 | - "v*" 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - scala: 2.11.12 20 | sbt-args: --addPluginSbtFile=project/plugins.sbt.scala-js.1.x 21 | - scala: 2.12.11 22 | sbt-args: --addPluginSbtFile=project/plugins.sbt.scala-js.1.x 23 | - scala: 2.13.4 24 | sbt-args: --addPluginSbtFile=project/plugins.sbt.scala-js.1.x 25 | - scala: 2.10.7 26 | - scala: 2.11.12 27 | - scala: 2.12.11 28 | - scala: 2.13.4 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 # Need the git history for sbt-dynver to determine the version 34 | - name: Set up JDK 11 35 | uses: actions/setup-java@v3 36 | with: 37 | java-version: "11" 38 | distribution: temurin 39 | - name: Cache SBT 40 | uses: actions/cache@v3 41 | with: 42 | path: | 43 | ~/.ivy2/local/ 44 | ~/.ivy2/cache/ 45 | ~/.sbt/ 46 | ~/.coursier/ 47 | key: | 48 | ${{runner.os}}-${{matrix.scala}}-${{hashFiles('**/*.sbt')}}-${{matrix.sbt-args}} 49 | ${{runner.os}}-${{matrix.scala}}-${{hashFiles('**/*.sbt')}}- 50 | ${{runner.os}}-${{matrix.scala}}- 51 | - name: Run tests 52 | run: sbt ${{matrix.sbt-args}} ++${{ matrix.scala }} test 53 | - name: Publish to Maven Central Repository 54 | env: 55 | GITHUB_PERSONAL_ACCESS_TOKEN: ${{secrets.PERSONAL_ACCESS_TOKEN}} 56 | if: ${{ env.GITHUB_PERSONAL_ACCESS_TOKEN != '' && github.event_name != 'pull_request' }} 57 | run: sbt ${{matrix.sbt-args}} ++${{ matrix.scala }} "set every Seq(sonatypeSessionName := \"${{github.workflow}} ${{github.run_id}}-${{github.run_number}}-${{github.run_attempt}}-$$ ${{ matrix.scala }}\", publishTo := sonatypePublishToBundle.value)" publishSigned sonatypeBundleRelease 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | local.sbt 3 | secret/ 4 | .metals/ 5 | .bloop/ 6 | metals.sbt 7 | .bsp/ 8 | .vscode/launch.json 9 | *.scala.semanticdb 10 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: igeolise/scalajs-test-runner:latest 2 | vscode: 3 | extensions: 4 | - scala-lang.scala@0.3.8:wQBBM+lKILHBqOqlqW60xA== 5 | - scalameta.metals@1.9.0:EyAIfy0ykjUn9htpw3f7GA== -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = scala212source3 2 | version = "3.7.1" 3 | maxColumn = 80 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2015 ThoughtWorks, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThoughtWorks Each 2 | 3 | [![Join the chat at https://gitter.im/ThoughtWorksInc/each](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ThoughtWorksInc/each?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/ThoughtWorksInc/each.svg?branch=3.3.x)](https://travis-ci.org/ThoughtWorksInc/each) 5 | [![Latest version](https://index.scala-lang.org/thoughtworksinc/each/each/latest.svg)](https://index.scala-lang.org/thoughtworksinc/each/each) 6 | 7 | **ThoughtWorks Each** is a macro library that converts native imperative syntax to [Scalaz](http://scalaz.org/)'s monadic expression. See the [object cats](https://javadoc.io/page/com.thoughtworks.dsl/dsl_2.12/latest/com/thoughtworks/dsl/domains/cats$.html) in [Dsl.scala](https://github.com/ThoughtWorksInc/Dsl.scala/) for the similar feature for [Cats](https://typelevel.org/cats/). 8 | 9 | ## Motivation 10 | 11 | There is a macro library [Stateless Future](https://github.com/qifun/stateless-future) that provides `await` for asynchronous programming. 12 | `await` is a mechanism that transform synchronous-like code into asynchronous expressions. C# 5.0, ECMAScript 7 and Python 3.5 also support the mechanism. 13 | 14 | The `await` mechanism in Stateless Future is implemented by an algorithm called [CPS transform](https://en.wikipedia.org/wiki/Continuation-passing_style). When learning [scalaz](https://scalaz.github.io/scalaz/), we found that the same algorithm could be applied for any monadic expression, including `Option` monad, `IO` monad, and `Future` monad. So we started this project, Each. 15 | 16 | Each is a superset of `await` syntax. Each supports multiple types of monads, while `await` only works with `Future`. When we perform a CPS transform for monadic expression with the `Future` monad, the use case looks almost the same as the `await` syntax in [Stateless Future](https://github.com/qifun/stateless-future). Each is like F#'s [Computation Expressions](https://msdn.microsoft.com/en-us/library/dd233182.aspx), except Each reuses the normal Scala syntax instead of reinventing new syntax. 17 | 18 | For example: 19 | 20 | ``` scala 21 | import com.thoughtworks.each.Monadic._ 22 | import scalaz.std.scalaFuture._ 23 | 24 | // Returns a Future of the sum of the length of each string in each parameter Future, 25 | // without blocking any thread. 26 | def concat(future1: Future[String], future2: Future[String]): Future[Int] = monadic[Future] { 27 | future1.each.length + future2.each.length 28 | } 29 | ``` 30 | 31 | The similar code works for monads other than `Future`: 32 | 33 | ``` scala 34 | import com.thoughtworks.each.Monadic._ 35 | import scalaz.std.option._ 36 | 37 | def plusOne(intOption: Option[Int]) = monadic[Option] { 38 | intOption.each + 1 39 | } 40 | assert(plusOne(None) == None) 41 | assert(plusOne(Some(15)) == Some(16)) 42 | ``` 43 | 44 | ``` scala 45 | import com.thoughtworks.each.Monadic._ 46 | import scalaz.std.list._ 47 | 48 | def plusOne(intSeq: List[Int]) = monadic[List] { 49 | intSeq.each + 1 50 | } 51 | assert(plusOne(Nil) == Nil) 52 | assert(plusOne(List(15)) == List(16)) 53 | assert(plusOne(List(15, -2, 9)) == List(16, -1, 10)) 54 | ``` 55 | 56 | ## Usage 57 | 58 | ### Step 1: Add the following line in your build.sbt 59 | 60 | ``` sbt 61 | libraryDependencies += "com.thoughtworks.each" %% "each" % "latest.release" 62 | 63 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 64 | ``` 65 | 66 | or `%%%` for Scala.js projects: 67 | 68 | ``` sbt 69 | libraryDependencies += "com.thoughtworks.each" %%% "each" % "latest.release" 70 | 71 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 72 | ``` 73 | 74 | Note that ThoughtWorks Each requires Scalaz 7.2.x and does not compatible with Scala 7.1.x . 75 | 76 | See https://repo1.maven.org/maven2/com/thoughtworks/each/ for a list of available versions. 77 | 78 | ### Step 2: In your source file, import `monadic` and `each` method 79 | 80 | ``` scala 81 | import com.thoughtworks.each.Monadic._ 82 | ``` 83 | 84 | ### Step 3: Import implicit Monad instances 85 | 86 | Scalaz has provided `Option` monad, so you just import it. 87 | 88 | ``` scala 89 | import com.thoughtworks.each.Monadic._ 90 | import scalaz.std.option._ 91 | ``` 92 | 93 | Please import other monad instances if you need other monads. 94 | 95 | ### Step 4: Use `monadic[F]` to create a monadic expression 96 | 97 | ``` scala 98 | import com.thoughtworks.each.Monadic._ 99 | import scalaz.std.option._ 100 | val result: Option[String] = monadic[Option] { 101 | "Hello, Each!" 102 | } 103 | ``` 104 | 105 | ### Step 5: In the `monadic` block, use `.each` postfix to extract each element in a `F` 106 | 107 | ``` scala 108 | import com.thoughtworks.each.Monadic._ 109 | import scalaz.std.option._ 110 | val name = Option("Each") 111 | val result: Option[String] = monadic[Option] { 112 | "Hello, " + name.each + "!" 113 | } 114 | ``` 115 | 116 | ## Exception handling 117 | 118 | `monadic` blocks do not support `try`, `catch` and `finally`. If you want these expressions, use `throwableMonadic` or `catchIoMonadic` instead, for example: 119 | 120 | ``` scala 121 | var count = 0 122 | val io = catchIoMonadic[IO] { 123 | count += 1 // Evaluates immediately 124 | val _ = IO(()).each // Pauses until io.unsafePerformIO() 125 | try { 126 | count += 1 127 | (null: Array[Int])(0) // Throws a NullPointerException 128 | } catch { 129 | case e: NullPointerException => { 130 | count += 1 131 | 100 132 | } 133 | } finally { 134 | count += 1 135 | } 136 | } 137 | assertEquals(1, count) 138 | assertEquals(100, io.unsafePerformIO()) 139 | assertEquals(4, count) 140 | ``` 141 | 142 | Note that `catchIoMonadic` requires an implicit parameter `scalaz.effect.MonadCatchIO[F]` instead of `Monad[F]`. `scalaz.effect.MonadCatchIO[F]` is only provided for `scalaz.effect.IO` by default. 143 | 144 | ## `for` loop 145 | 146 | Each supports `.each` magic in a `for` loop on any instances that support `Foldable` type class. For example, you could `import scalaz.std.list._` to enable the `Foldable` type class for `List`. 147 | 148 | ``` scala 149 | import com.thoughtworks.each.Monadic._ 150 | import scalaz.std.list._ 151 | import scalaz.std.option._ 152 | val n = Some(10) 153 | @monadic[Option] val result = { 154 | var count = 1 155 | for (i <- List(300, 20)) { 156 | count += i * n.each 157 | } 158 | count 159 | } 160 | Assert.assertEquals(Some(3201), result) 161 | ``` 162 | 163 | Note that you need to use `@monadic[Option]` annotation instead of `monadic[Option]` block to in order to enable the `for` loop syntax. 164 | 165 | ## `for` comprehension 166 | 167 | Each also supports `.each` magic in a `for` comprehension on any instances that support `Traverse` and `MonadPlus` type class. 168 | 169 | ``` scala 170 | import com.thoughtworks.each.Monadic._ 171 | import scalaz.std.list._ 172 | val n = Some(4000) 173 | @monadic[Option] val result = { 174 | for { 175 | i <- List(300, 20) 176 | (j, k) <- List(50000 -> "1111", 600000 -> "yyy") 177 | if i > n.each - 3900 178 | a = i + j 179 | } yield { 180 | a + n.each * k.length 181 | } 182 | } 183 | Assert.assertEquals(Some(List(66300, 612300)), result) 184 | ``` 185 | 186 | Note that you need to use `@monadic[Option]` annotation instead of `monadic[Option]` block to in order to enable the `for` comprehension syntax. 187 | 188 | ## Limitation 189 | 190 | If a [call-by-name parameter](http://www.scala-lang.org/files/archive/spec/2.11/06-expressions.html#function-applications) of a method call is a monadic expression, `Each` will transform the monadic expression before the method call. The behavior was discussed at [#37](https://github.com/ThoughtWorksInc/each/issues/37). 191 | 192 | ```scala 193 | def innerFailureFuture = Future.failed(new Exception("foo")) 194 | val someValue = Some("value") 195 | val result = monadic[Future] { 196 | someValue.getOrElse(innerFailureFuture.each) 197 | } 198 | ``` 199 | 200 | `result` will be a future of failure because the above example equals to 201 | 202 | ```scala 203 | def innerFailureFuture = Future.failed(new Exception("foo")) 204 | val someValue = Some("value") 205 | val result = innerFailureFuture.map(someValue.getOrElse) 206 | ``` 207 | 208 | `innerFailureFuture.each` is evaluated before being passed to `getOrElse` method call, even if `getOrElse` accepts a call-by-name parameter. 209 | 210 | ## Links 211 | 212 | * [The API Documentation](https://www.javadoc.io/doc/com.thoughtworks.each/each_2.13/latest/com/thoughtworks/each/Monadic$.html) 213 | * Utilities 214 | * [ComprehensionMonad](https://github.com/ThoughtWorksInc/each/wiki/ComprehensionMonad) 215 | 216 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "com.thoughtworks.each" 2 | 3 | publish / skip := true 4 | 5 | // Workaround for randomly Travis CI fail 6 | parallelExecution in Global := false 7 | 8 | fork in Global in compile := true 9 | 10 | description in ThisBuild := "A collection of Scala language extension for specific domains." 11 | 12 | lazy val each = sbtcrossproject.CrossPlugin.autoImport.crossProject 13 | .crossType(sbtcrossproject.CrossPlugin.autoImport.CrossType.Pure) 14 | 15 | lazy val eachJVM = each.jvm 16 | 17 | lazy val eachJS = each.js 18 | 19 | startYear in ThisBuild := Some(2015) 20 | -------------------------------------------------------------------------------- /each/.js/build.sbt: -------------------------------------------------------------------------------- 1 | ../build.sbt.shared -------------------------------------------------------------------------------- /each/.jvm/build.sbt: -------------------------------------------------------------------------------- 1 | ../build.sbt.shared -------------------------------------------------------------------------------- /each/build.sbt.shared: -------------------------------------------------------------------------------- 1 | scalacOptions += "-deprecation" 2 | 3 | scalacOptions += "-feature" 4 | 5 | scalacOptions += "-unchecked" 6 | 7 | description := "A macro library that converts native imperative syntax to scalaz's monadic expressions." 8 | 9 | libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % Test 10 | 11 | libraryDependencies ++= { 12 | if (scalaVersion.value.startsWith("2.10.")) { 13 | Seq() 14 | } else { 15 | Seq("org.scala-lang.modules" %% "scala-xml" % "1.3.0" % Test) 16 | } 17 | } 18 | 19 | libraryDependencies ++= PartialFunction.condOpt(VersionNumber(scalaVersion.value).matchesSemVer(SemanticSelector("<2.13"))) { 20 | case true => 21 | compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) 22 | } 23 | 24 | scalacOptions ++= PartialFunction.condOpt(scalaBinaryVersion.value) { 25 | case "2.13" => 26 | "-Ymacro-annotations" 27 | } 28 | 29 | libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided 30 | 31 | libraryDependencies += { 32 | if (scalaBinaryVersion.value == "2.10") { 33 | "com.thoughtworks.sde" %%% "core" % "3.3.2" 34 | } else { 35 | "com.thoughtworks.sde" %%% "core" % "3.3.4" 36 | } 37 | } 38 | 39 | libraryDependencies += "com.thoughtworks.sde" %%% "comprehension-monad" % "3.3.2" 40 | 41 | // Disable partial-unification due to compiler crash in Scala 2.10 42 | disablePlugins(PartialUnification) 43 | -------------------------------------------------------------------------------- /each/src/main/scala-2.13/com/thoughtworks/each/macrocompat.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks.each 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | private[each] object macrocompat { 6 | class bundle extends StaticAnnotation 7 | } -------------------------------------------------------------------------------- /each/src/main/scala/com/thoughtworks/each/Monadic.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 ThoughtWorks, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.thoughtworks.each 18 | 19 | import com.thoughtworks.sde.core.{MonadicFactory, Preprocessor} 20 | import macrocompat.bundle 21 | 22 | import scala.annotation.{StaticAnnotation, compileTimeOnly} 23 | import scala.language.experimental.macros 24 | import scala.language.{higherKinds, implicitConversions} 25 | import scala.reflect.macros._ 26 | import scalaz._ 27 | import scalaz.effect.MonadCatchIO 28 | import scalaz.syntax.{FoldableOps, MonadPlusOps, TraverseOps} 29 | 30 | /** 31 | * @author 杨博 (Yang Bo) <pop.atry@gmail.com> 32 | */ 33 | object Monadic { 34 | 35 | 36 | @inline 37 | implicit final class ToMonadicLoopOps[F[_], A](underlying: F[A]) { 38 | 39 | def monadicLoop = new MonadicLoop(underlying) 40 | 41 | } 42 | 43 | @inline 44 | implicit def getUnderlying[F[_], A](monadicLoop: MonadicLoop[F, A]): F[A] = monadicLoop.underlying 45 | 46 | object MonadicLoop { 47 | 48 | @bundle 49 | private[MonadicLoop] final class MacroBundle(val c: blackbox.Context) { 50 | 51 | import c.universe._ 52 | 53 | def foreach(f: Tree)(foldable: Tree): Tree = { 54 | val q"$monadicLoop.foreach[$u]($f)($foldable)" = c.macroApplication 55 | val monadicLoopName = TermName(c.freshName("monadicLoop")) 56 | q""" 57 | val $monadicLoopName = $monadicLoop 58 | _root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.foreach[ 59 | $monadicLoopName.F, 60 | $monadicLoopName.Element, 61 | $u 62 | ]($monadicLoopName.underlying, $foldable, $f) 63 | """ 64 | } 65 | 66 | def map(f: Tree)(traverse: Tree): Tree = { 67 | val q"$monadicLoop.map[$b]($f)($traverse)" = c.macroApplication 68 | val monadicLoopName = TermName(c.freshName("monadicLoop")) 69 | q""" 70 | val $monadicLoopName = $monadicLoop 71 | new _root_.com.thoughtworks.each.Monadic.MonadicLoop[$monadicLoopName.F, $b]( 72 | _root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.map[ 73 | $monadicLoopName.F, 74 | $monadicLoopName.Element, 75 | $b 76 | ]($monadicLoopName.underlying, $traverse, $f) 77 | ) 78 | """ 79 | } 80 | 81 | def flatMap(f: Tree)(traverse: Tree, bind: Tree): Tree = { 82 | val q"$monadicLoop.flatMap[$b]($f)($traverse, $bind)" = c.macroApplication 83 | val monadicLoopName = TermName(c.freshName("monadicLoop")) 84 | q""" 85 | val $monadicLoopName = $monadicLoop 86 | new _root_.com.thoughtworks.each.Monadic.MonadicLoop[$monadicLoopName.F, $b]( 87 | _root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.flatMap[ 88 | $monadicLoopName.F, 89 | $monadicLoopName.Element, 90 | $b 91 | ]($monadicLoopName.underlying, $traverse, $bind, $f) 92 | ) 93 | """ 94 | } 95 | 96 | def filter(f: Tree)(traverse: Tree, monadPlus: Tree): Tree = { 97 | val q"$monadicLoop.${TermName("filter" | "withFilter")}($f)($traverse, $monadPlus)" = c.macroApplication 98 | val monadicLoopName = TermName(c.freshName("monadicLoop")) 99 | q""" 100 | val $monadicLoopName = $monadicLoop 101 | new _root_.com.thoughtworks.each.Monadic.MonadicLoop[$monadicLoopName.F, $monadicLoopName.Element]( 102 | _root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.filter[ 103 | $monadicLoopName.F, 104 | $monadicLoopName.Element 105 | ]($monadicLoopName.underlying, $traverse, $monadPlus, $f) 106 | ) 107 | """ 108 | } 109 | 110 | } 111 | 112 | @inline 113 | implicit def toFoldableOps[F[_] : Foldable, A](monadicLoop: MonadicLoop[F, A]): FoldableOps[F, A] = { 114 | scalaz.syntax.foldable.ToFoldableOps(monadicLoop.underlying) 115 | } 116 | 117 | @inline 118 | implicit def toTraverseOps[F[_] : Traverse, A](monadicLoop: MonadicLoop[F, A]): TraverseOps[F, A] = { 119 | scalaz.syntax.traverse.ToTraverseOps(monadicLoop.underlying) 120 | } 121 | 122 | @inline 123 | implicit def toMonadPlusOps[F[_] : MonadPlus, A](monadicLoop: MonadicLoop[F, A]): MonadPlusOps[F, A] = { 124 | scalaz.syntax.monadPlus.ToMonadPlusOps(monadicLoop.underlying) 125 | } 126 | 127 | } 128 | 129 | @deprecated( 130 | message = """ 131 | Use `@monadic[X] def f = { ... }` instead of `monadic[X] { ... }`. 132 | Note that you can remove `.monadicLoop` in `@monadic` methods. 133 | """, 134 | since = "1.0.1") 135 | final class MonadicLoop[F0[_], A](val underlying: F0[A]) { 136 | 137 | type F[X] = F0[X] 138 | 139 | type Element = A 140 | 141 | @inline 142 | def toFoldableOps(implicit foldable: Foldable[F]) = scalaz.syntax.foldable.ToFoldableOps(underlying) 143 | 144 | @inline 145 | def toTraverseOps(implicit traverse: Traverse[F]) = scalaz.syntax.traverse.ToTraverseOps(underlying) 146 | 147 | def foreach[U](f: A => U)(implicit foldable: Foldable[F]): Unit = macro MonadicLoop.MacroBundle.foreach 148 | 149 | def map[B](f: A => B)(implicit traverse: Traverse[F]): MonadicLoop[F, B] = macro MonadicLoop.MacroBundle.map 150 | 151 | def flatMap[B](f: A => F[B])(implicit traverse: Traverse[F], bind: Bind[F]): MonadicLoop[F, B] = macro MonadicLoop.MacroBundle.flatMap 152 | 153 | def filter(f: A => Boolean)(implicit traverse: Traverse[F], monadPlus: MonadPlus[F]): MonadicLoop[F, A] = macro MonadicLoop.MacroBundle.filter 154 | 155 | def withFilter(f: A => Boolean)(implicit traverse: Traverse[F], monadPlus: MonadPlus[F]): MonadicLoop[F, A] = macro MonadicLoop.MacroBundle.filter 156 | 157 | } 158 | 159 | /** 160 | * An implicit view to enable `for` `yield` comprehension for a monadic value. 161 | * 162 | * @param v the monadic value. 163 | * @param F0 a helper to infer types. 164 | * @tparam FA type of the monadic value. 165 | * @return the temporary wrapper that contains the `each` method. 166 | */ 167 | @inline 168 | implicit def toMonadicLoopOpsUnapply[FA](v: FA)(implicit F0: Unapply[Foldable, FA]) = { 169 | new ToMonadicLoopOps[F0.M, F0.A](F0(v)) 170 | } 171 | 172 | object EachOps { 173 | 174 | @bundle 175 | private[EachOps] final class MacroBundle(val c: whitebox.Context) { 176 | 177 | import c.universe._ 178 | 179 | def each: Tree = { 180 | val q"$ops.each" = c.macroApplication 181 | val opsName = TermName(c.freshName("ops")) 182 | q""" 183 | val $opsName = $ops 184 | _root_.com.thoughtworks.sde.core.MonadicFactory.Instructions.each[ 185 | $opsName.M, 186 | $opsName.A 187 | ]($opsName.underlying) 188 | """ 189 | } 190 | } 191 | 192 | } 193 | 194 | /** 195 | * The temporary wrapper that contains the `each` method. 196 | * 197 | * @param underlying the underlying monadic value. 198 | * @tparam M0 the higher kinded type of the monadic value. 199 | * @tparam A0 the element type of of the monadic value. 200 | */ 201 | final case class EachOps[M0[_], A0](underlying: M0[A0]) { 202 | 203 | type M[A] = M0[A] 204 | 205 | type A = A0 206 | 207 | /** 208 | * Semantically, returns the result in the monadic value. 209 | * 210 | * This macro must be inside a `monadic` 211 | * or a `catchIoMonadic` block. 212 | * 213 | * This is not a real method, thus it will never actually execute. 214 | * Instead, the call to this method will be transformed to a monadic expression. 215 | * The actually result is passing as a parameter to some [[scalaz.Monad#bind]] and [[scalaz.Monad#point]] calls 216 | * instead of as a return value. 217 | * 218 | * @return the result in the monadic value. 219 | */ 220 | def each: A = macro EachOps.MacroBundle.each 221 | 222 | } 223 | 224 | /** 225 | * An implicit view to enable `.each` for a monadic value. 226 | * 227 | * @param v the monadic value. 228 | * @param F0 a helper to infer types. 229 | * @tparam FA type of the monadic value. 230 | * @return the temporary wrapper that contains the `each` method. 231 | */ 232 | @inline 233 | implicit def toEachOpsUnapply[FA](v: FA)(implicit F0: Unapply[Bind, FA]): EachOps[F0.M, F0.A] = new EachOps[F0.M, F0.A](F0(v)) 234 | 235 | /** 236 | * An implicit view to enable `.each` for a monadic value. 237 | * 238 | * @param v the monadic value. 239 | * @return the temporary wrapper that contains the `each` method. 240 | */ 241 | @inline 242 | implicit def toEachOps[F[_], A](v: F[A]): EachOps[F, A] = new EachOps(v) 243 | 244 | @bundle 245 | final class AnnotationBundle(context: whitebox.Context) extends Preprocessor(context) { 246 | 247 | import c.universe._ 248 | 249 | 250 | private def macroTransform(m: Tree, annottees: Seq[Tree]): Tree = { 251 | 252 | val (f, tc) = c.macroApplication match { 253 | case q"new $annotationClass[$f]()($tc).macroTransform(..$annottees)" => 254 | (f, tc) 255 | case q"new $annotationClass[$f]($tc).macroTransform(..$annottees)" => 256 | (f, tc) 257 | case q"new $annotationClass[$f]().macroTransform(..$annottees)" => 258 | (f, q"_root_.scala.Predef.implicitly[$m[$f]]") 259 | } 260 | 261 | val eachOpsName = TermName(c.freshName("eachOps")) 262 | val toEachOpsName = TermName(c.freshName("ToEachOps")) 263 | 264 | replaceDefBody(annottees, { body => 265 | q""" 266 | _root_.com.thoughtworks.sde.core.MonadicFactory[ 267 | $m, 268 | $f 269 | ].apply { 270 | object $toEachOpsName { 271 | import scala.language.implicitConversions 272 | implicit def $eachOpsName[A](fa: $f[A]): _root_.com.thoughtworks.each.Monadic.EachOps[$f, A] = { 273 | new _root_.com.thoughtworks.each.Monadic.EachOps[$f, A](fa) 274 | } 275 | } 276 | import $toEachOpsName.$eachOpsName 277 | ${(new ComprehensionTransformer).transform(body)} 278 | }($tc) 279 | """ 280 | }) 281 | } 282 | 283 | def throwableMonadic(annottees: Tree*): Tree = { 284 | macroTransform(tq"_root_.com.thoughtworks.each.Monadic.MonadThrowable", annottees) 285 | } 286 | 287 | def monadic(annottees: Tree*): Tree = { 288 | macroTransform(tq"_root_.scalaz.Monad", annottees) 289 | } 290 | 291 | def catchIoMonadic(annottees: Tree*): Tree = { 292 | macroTransform(tq"_root_.scalaz.MonadCatchIO", annottees) 293 | } 294 | 295 | } 296 | 297 | /** 298 | * @usecase def monadic[F[_]](body: AnyRef)(implicit monad: Monad[F]): F[body.type] = ??? 299 | * 300 | * Captures all the result in the `body` and converts them into a `F`. 301 | * 302 | * Note that `body` must not contain any `try` / `catch` / `throw` expressions. 303 | * @tparam F the higher kinded type of the monadic expression. 304 | * @param body the imperative style expressions that will be transform to monadic style. 305 | * @param monad the monad that executes expressions in `body`. 306 | * @return 307 | */ 308 | @inline 309 | def monadic[F[_]] = MonadicFactory[Monad, F] 310 | 311 | @compileTimeOnly("enable macro paradise to expand macro annotations") 312 | final class monadic[F[_]] extends StaticAnnotation { 313 | def macroTransform(annottees: Any*): Any = macro AnnotationBundle.monadic 314 | } 315 | 316 | /** 317 | * @usecase def catchIoMonadic[F[_]](body: AnyRef)(implicit monad: MonadCatchIO[F]): F[body.type] = ??? 318 | * 319 | * Captures all the result in the `body` and converts them into a `F`. 320 | * 321 | * Note that `body` may contain any `try` / `catch` / `throw` expressions. 322 | * @tparam F the higher kinded type of the monadic expression. 323 | * @param body the imperative style expressions that will be transform to monadic style. 324 | * @param monad the monad that executes expressions in `body`. 325 | * @return 326 | */ 327 | @inline 328 | def catchIoMonadic[F[_]] = MonadicFactory[MonadCatchIO, F] 329 | 330 | @compileTimeOnly("enable macro paradise to expand macro annotations") 331 | final class catchIoMonadic[F[_]] extends StaticAnnotation { 332 | def macroTransform(annottees: Any*): Any = macro AnnotationBundle.catchIoMonadic 333 | } 334 | 335 | // TODO: create Unapply instead 336 | @inline 337 | implicit def eitherTMonadThrowable[F[_], G[_[_], _]](implicit F0: Monad[({type g[y] = G[F, y]})#g]): MonadThrowable[ 338 | ({type f[x] = EitherT[({type g[y] = G[F, y]})#g, Throwable, x]})#f 339 | ] = { 340 | EitherT.eitherTMonadError[({type g[y] = G[F, y]})#g, Throwable] 341 | } 342 | 343 | @inline 344 | implicit def lazyEitherTMonadThrowable[F[_], G[_[_], _]](implicit F0: Monad[({type g[y] = G[F, y]})#g]): MonadThrowable[ 345 | ({type f[x] = LazyEitherT[({type g[y] = G[F, y]})#g, Throwable, x]})#f 346 | ] = { 347 | LazyEitherT.lazyEitherTMonadError[({type g[y] = G[F, y]})#g, Throwable] 348 | } 349 | 350 | 351 | /** 352 | * A [[scalaz.Monad]] that supports exception handling. 353 | * 354 | * Note this is a simplified version of [[scalaz.MonadError]]. 355 | * 356 | * @tparam F the higher kinded type of the monad. 357 | */ 358 | type MonadThrowable[F[_]] = MonadError[F, Throwable] 359 | 360 | /** 361 | * @usecase def throwableMonadic[F[_]](body: AnyRef)(implicit monad: MonadThrowable[F]): F[body.type] = ??? 362 | * 363 | * Captures all the result in the `body` and converts them into a `F`. 364 | * 365 | * Note that `body` may contain any `try` / `catch` / `throw` expressions. 366 | * @tparam F the higher kinded type of the monadic expression. 367 | * @param body the imperative style expressions that will be transform to monadic style. 368 | * @param monad the monad that executes expressions in `body`. 369 | * @return 370 | */ 371 | @inline 372 | def throwableMonadic[F[_]] = MonadicFactory[MonadThrowable, F] 373 | 374 | @compileTimeOnly("enable macro paradise to expand macro annotations") 375 | final class throwableMonadic[F[_]] extends StaticAnnotation { 376 | def macroTransform(annottees: Any*): Any = macro AnnotationBundle.throwableMonadic 377 | } 378 | 379 | } 380 | -------------------------------------------------------------------------------- /each/src/main/scala/com/thoughtworks/each/package.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks 2 | 3 | /** 4 | * @author 杨博 (Yang Bo) <pop.atry@gmail.com> 5 | */ 6 | package object each { 7 | 8 | /** 9 | * Contains implicit methods to work with types that support `for`/`yield` comprehension. 10 | */ 11 | val ComprehensionImplicits = sde.comprehensionMonad.ComprehensionMonad 12 | 13 | } 14 | -------------------------------------------------------------------------------- /each/src/test/scala-2.11/com/thoughtworks/each/ReportingTest.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks.each 2 | 3 | import com.thoughtworks.each.Monadic._ 4 | import org.junit.{Assert, Test} 5 | 6 | import scala.concurrent.duration.Duration 7 | import scala.concurrent.{Await, Future} 8 | import scalaz._ 9 | import scalaz.std.list._ 10 | import scalaz.syntax.traverse._ 11 | 12 | 13 | class ReportingTest { 14 | 15 | private trait AppAction[A] 16 | 17 | private case object GetEmailList extends AppAction[Throwable \/ List[String]] 18 | 19 | private case class GetContactNameByEmail(email: String) extends AppAction[Throwable \/ String] 20 | 21 | private type FreeCommand[A] = Free[AppAction, A] 22 | 23 | private type Script[A] = EitherT[FreeCommand, Throwable, A] 24 | 25 | private def toScript[A](action: AppAction[Throwable \/ A]): Script[A] = { 26 | new Script[A](Free.liftF(action)) 27 | } 28 | 29 | import scala.language.{higherKinds, implicitConversions} 30 | 31 | private implicit def cast[From, To](from: Script[From])(implicit view: From => To): Script[To] = { 32 | Monad[Script].map[From, To](from)(view) 33 | } 34 | 35 | private def eachScript: Script[xml.Elem] = throwableMonadic[Script] { 36 | val emailList = toScript(GetEmailList).each 37 | 38 | 39 | { 40 | (for { 41 | email: String <- emailList.monadicLoop 42 | if email.matches( """[a-z.\-_]+@[a-z.\-_]+""") 43 | } yield { 44 | 45 | 48 | 51 | 52 | }).toList 53 | }
46 | {toScript(GetContactNameByEmail(email)).each} 47 | 49 | {email} 50 |
54 | 55 | 56 | } 57 | 58 | private def rawScript: Script[xml.Elem] = { 59 | toScript(GetEmailList).flatMap { emailList => 60 | emailList.traverseM[Script, xml.Elem] { email => 61 | toScript(GetContactNameByEmail(email)).map { name => 62 | if (email.matches( """[^@]+@[^@]+""")) { 63 | List( 64 | 65 | {name} 66 | 67 | 68 | {email} 69 | 70 | ) 71 | } else { 72 | Nil 73 | } 74 | } 75 | }.map { trs => 76 | 77 | 78 | 79 | {trs} 80 |
81 | 82 | 83 | } 84 | } 85 | } 86 | 87 | @Test 88 | def testReporting(): Unit = { 89 | val Data = Map( 90 | "atryyang@thoughtworks.com" -> "Yang Bo", 91 | "invalid-mail-address" -> "N/A", 92 | "john.smith@gmail.com" -> "John Smith" 93 | ) 94 | val interpreter = new (AppAction ~> scalaz.Id.Id) { 95 | override def apply[A](fa: AppAction[A]): A = { 96 | fa match { 97 | case GetEmailList => { 98 | \/-(Data.keys.toList) 99 | } 100 | case GetContactNameByEmail(email) =>Data.get(email) match { 101 | case None => { 102 | -\/(new NoSuchElementException) 103 | } 104 | case Some(name) => { 105 | \/-(name) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | val rawHtml = 113 | xml.Xhtml.toXhtml(xml.Utility.trim(rawScript.run.foldMap(interpreter).fold(throw _, identity))) 114 | 115 | val eachHtml = 116 | xml.Xhtml.toXhtml(xml.Utility.trim(eachScript.run.foldMap(interpreter).fold(throw _, identity))) 117 | 118 | Assert.assertEquals(rawHtml, eachHtml) 119 | 120 | } 121 | 122 | @Test 123 | def testAsyncReporting(): Unit = { 124 | val Data = Map( 125 | "atryyang@thoughtworks.com" -> "Yang Bo", 126 | "invalid-mail-address" -> "N/A", 127 | "john.smith@gmail.com" -> "John Smith" 128 | ) 129 | val interpreter = new (AppAction ~> Future) { 130 | override def apply[A](fa: AppAction[A]): Future[A] = { 131 | fa match { 132 | case GetEmailList => { 133 | Future.successful(\/-(Data.keys.toList)) 134 | } 135 | case GetContactNameByEmail(email) => Future.successful(Data.get(email) match { 136 | case None => { 137 | -\/(new NoSuchElementException) 138 | } 139 | case Some(name) => { 140 | \/-(name) 141 | } 142 | }) 143 | } 144 | } 145 | } 146 | 147 | import scala.concurrent.ExecutionContext.Implicits.global 148 | import scalaz.std.scalaFuture._ 149 | 150 | val rawHtml = 151 | xml.Xhtml.toXhtml(xml.Utility.trim(Await.result(rawScript.run.foldMap(interpreter), Duration.Inf).fold(throw _, identity))) 152 | 153 | val eachHtml = 154 | xml.Xhtml.toXhtml(xml.Utility.trim(Await.result(eachScript.run.foldMap(interpreter), Duration.Inf).fold(throw _, identity))) 155 | 156 | Assert.assertEquals(rawHtml, eachHtml) 157 | 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /each/src/test/scala-2.11/com/thoughtworks/each/TraverseComprehensionTest211.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks.each 2 | 3 | import com.thoughtworks.each.Monadic._ 4 | import org.junit.{Assert, Test} 5 | 6 | import scalaz.std.list._ 7 | import scalaz.std.option._ 8 | 9 | class TraverseComprehensionTest211 { 10 | 11 | @Test 12 | def testFilter(): Unit = { 13 | val n = Some(4000) 14 | 15 | val result = monadic[Option] { 16 | (for { 17 | i <- List(300, 20).monadicLoop 18 | if i > 100 19 | } yield { 20 | i + n.each 21 | }).underlying 22 | } 23 | Assert.assertEquals(Some(List(4300)), result) 24 | } 25 | 26 | @Test 27 | def testComplex(): Unit = { 28 | val n = Some(4000) 29 | val result = monadic[Option] { 30 | (for { 31 | i <- List(300, 20).monadicLoop 32 | (j, k) <- List(50000 -> "1111", 600000 -> "yyy").monadicLoop 33 | if i > n.each - 3900 34 | a = i + j 35 | } yield { 36 | a + n.each * k.length 37 | }).underlying 38 | } 39 | 40 | Assert.assertEquals(Some(List(66300, 612300)), result) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/AnnotationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 ThoughtWorks, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.thoughtworks.each 18 | 19 | import com.thoughtworks.each.Monadic.monadic 20 | import org.junit.{Assert, Test} 21 | import scalaz.std.option._ 22 | import scalaz.std.list._ 23 | 24 | /** 25 | * @author 杨博 (Yang Bo) <pop.atry@gmail.com> 26 | */ 27 | class AnnotationTest { 28 | 29 | @Test 30 | def testForeach(): Unit = { 31 | val n = Some(10) 32 | @monadic[Option] val result = { 33 | var count = 1 34 | for (i <- List(300, 20)) { 35 | count += i * n.each 36 | } 37 | count 38 | } 39 | Assert.assertEquals(Some(3201), result) 40 | } 41 | 42 | @Test 43 | def testMap(): Unit = { 44 | val n = Some(4000) 45 | @monadic[Option] val result = { 46 | for (i <- List(300, 20)) yield { 47 | i + n.each 48 | } 49 | } 50 | Assert.assertEquals(Some(List(4300, 4020)), result) 51 | } 52 | 53 | @Test 54 | def testFlatMap(): Unit = { 55 | val n = Some(4000) 56 | @monadic[Option] val result = { 57 | for { 58 | i <- List(300, 20) 59 | j <- List(50000, 600000) 60 | } yield { 61 | i + j + n.each 62 | } 63 | } 64 | Assert.assertEquals(Some(List(54300, 604300, 54020, 604020)), result) 65 | } 66 | 67 | @Test 68 | def testFilter(): Unit = { 69 | val n = Some(4000) 70 | 71 | @monadic[Option] val result = { 72 | for { 73 | i <- List(300, 20) 74 | if i > 100 75 | } yield { 76 | i + n.each 77 | } 78 | } 79 | Assert.assertEquals(Some(List(4300)), result) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/ComprehensionImplicitsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 ThoughtWorks, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.thoughtworks.each 18 | 19 | import org.junit.{Assert, Test} 20 | 21 | class ComprehensionImplicitsTest { 22 | 23 | @Test 24 | def testListPoint(): Unit = { 25 | val seqApplicative = ComprehensionImplicits.comprehensionMonad[List] 26 | Assert.assertEquals(List("hello, applicative"), seqApplicative.point("hello, applicative")) 27 | } 28 | 29 | @Test 30 | def testSeqPoint(): Unit = { 31 | val seqApplicative = ComprehensionImplicits.comprehensionMonad[Seq] 32 | Assert.assertEquals(Seq("hello, applicative"), seqApplicative.point("hello, applicative")) 33 | } 34 | 35 | @Test 36 | def testSeqAp(): Unit = { 37 | val seqApplicative = ComprehensionImplicits.comprehensionMonad[Seq] 38 | Assert.assertEquals(Seq("Hello1!", "Hello2!", "Hello1?", "Hello2?"), seqApplicative.ap(Seq("Hello1", "Hello2"))(Seq((_: String) + "!", (_: String) + "?"))) 39 | } 40 | 41 | @Test 42 | def testOptionMap(): Unit = { 43 | val optionBind = ComprehensionImplicits.comprehensionMonad[Option] 44 | Assert.assertEquals( 45 | Option("hello, applicative"), optionBind.map(Option("hello, ")) { a => 46 | a + "applicative" 47 | }) 48 | } 49 | 50 | @Test 51 | def testOptionBind(): Unit = { 52 | val optionBind = ComprehensionImplicits.comprehensionMonad[Option] 53 | Assert.assertEquals( 54 | Option("hello, applicative"), 55 | optionBind.bind(Option("hello, ")) { a => 56 | Option(a + "applicative") 57 | }) 58 | } 59 | 60 | @Test 61 | def testSeqMap(): Unit = { 62 | val seqBind = ComprehensionImplicits.comprehensionMonad[Seq] 63 | Assert.assertEquals( 64 | Seq("hello, applicative"), seqBind.map(Seq("hello, ")) { a => 65 | a + "applicative" 66 | }) 67 | } 68 | 69 | 70 | @Test 71 | def testSeqBind(): Unit = { 72 | val seqBind = ComprehensionImplicits.comprehensionMonad[Seq] 73 | Assert.assertEquals( 74 | Seq("hello, applicative"), 75 | seqBind.bind(Seq("hello, ")) { a => 76 | Seq(a + "applicative") 77 | }) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/Issue38.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks.each 2 | 3 | import scalaz.std.option._ 4 | import Monadic._ 5 | 6 | final class Issue38 { 7 | def shouldNotWarning = { 8 | monadic[Option] { 9 | Option( 10 | { 11 | val mm = 1 12 | mm 13 | }).each; 14 | 1 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/MonadicErrorTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 ThoughtWorks, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.thoughtworks.each 18 | 19 | import java.io.IOException 20 | 21 | import org.junit.{Assert, Test} 22 | import Monadic.{throwableMonadic, _} 23 | 24 | import scalaz._ 25 | import scala.language.higherKinds 26 | import scala.language.existentials 27 | import scala.language.implicitConversions 28 | import scalaz.std.option._ 29 | 30 | class MonadicErrorTest { 31 | 32 | @Test 33 | def testAnnotation(): Unit = { 34 | 35 | case object MyException extends Exception 36 | 37 | type OptionScript[A] = EitherT[Option, Throwable, A] 38 | 39 | val either = { 40 | var count = 0 41 | 42 | import scala.language.implicitConversions 43 | implicit def cast[From, To](from: OptionScript[From])(implicit view: From => To): OptionScript[To] = { 44 | Monad[OptionScript].map[From, To](from)(view) 45 | } 46 | 47 | @throwableMonadic[OptionScript] 48 | val either = { 49 | try { 50 | count += 1 51 | throw MyException 52 | count += 10 53 | throw new Exception("Unreachable code") 54 | } catch { 55 | case MyException => { 56 | count += 100 57 | count 58 | } 59 | } finally { 60 | count += 1000 61 | } 62 | } 63 | Assert.assertEquals(1101, count) 64 | either 65 | } 66 | 67 | Assert.assertEquals(Some(\/-(101)), either.run) 68 | } 69 | 70 | @Test 71 | def testTryCatchOption(): Unit = { 72 | 73 | case object MyException extends Exception 74 | 75 | type OptionScript[A] = EitherT[Option, Throwable, A] 76 | 77 | val either = { 78 | var count = 0 79 | 80 | import scala.language.implicitConversions 81 | implicit def cast[From, To](from: OptionScript[From])(implicit view: From => To): OptionScript[To] = { 82 | Monad[OptionScript].map[From, To](from)(view) 83 | } 84 | 85 | val either = throwableMonadic[OptionScript] { 86 | try { 87 | count += 1 88 | throw MyException 89 | count += 10 90 | throw new Exception("Unreachable code") 91 | } catch { 92 | case MyException => { 93 | count += 100 94 | count 95 | } 96 | } finally { 97 | count += 1000 98 | } 99 | } 100 | Assert.assertEquals(1101, count) 101 | either 102 | } 103 | 104 | Assert.assertEquals(Some(\/-(101)), either.run) 105 | } 106 | 107 | private trait Command[A] 108 | 109 | private case object RandomInt extends Command[Throwable \/ Int] 110 | 111 | private case class Count(delta: Int) extends Command[Throwable \/ Int] 112 | 113 | private type FreeCommand[A] = Free[Command, A] 114 | 115 | private type Script[A] = EitherT[FreeCommand, Throwable, A] 116 | 117 | private val randomInt: Script[Int] = EitherT[FreeCommand, Throwable, Int](Free.liftF(RandomInt)) 118 | 119 | private def count(delta: Int): Script[Int] = EitherT[FreeCommand, Throwable, Int](Free.liftF(Count(delta))) 120 | 121 | private case object MyException extends Exception 122 | 123 | def noScript(randomInt: () => Int) = { 124 | var count = 0 125 | count += 50000 126 | try { 127 | if (randomInt() > 100) { 128 | count += 600000 129 | throw MyException 130 | count += 7000000 131 | 789 132 | } else if (randomInt() > 10) { 133 | count += 1 134 | 123 135 | } else { 136 | count += 20 137 | (throw new IOException): Int 138 | } 139 | } catch { 140 | case e: IOException => { 141 | count += 300 142 | 456 143 | } 144 | } finally { 145 | count += 4000 146 | } 147 | } 148 | 149 | private def newScriptWithoutEach: Script[Int] = { 150 | import scalaz.syntax.monadError._ 151 | 152 | count(50000).flatMap { _ => 153 | implicitly[MonadThrowable[Script]].handleError { 154 | Monad[Script].ifM( 155 | randomInt map { 156 | _ > 100 157 | }, { 158 | count(600000).flatMap { _ => 159 | implicitly[MonadThrowable[Script]].raiseError(MyException) flatMap { _: Nothing => 160 | count(7000000).map { _ => 161 | 789 162 | } 163 | } 164 | } 165 | }, { 166 | Monad[Script].ifM(randomInt map { 167 | _ > 100 168 | }, { 169 | count(1).map { _ => 170 | 123 171 | } 172 | }, { 173 | count(20).flatMap { _ => 174 | implicitly[MonadThrowable[Script]].raiseError(new IOException) map { x: Nothing => 175 | x: Int 176 | } 177 | } 178 | }) 179 | } 180 | ) 181 | } { 182 | case e: IOException => { 183 | count(300).map { _ => 184 | 456 185 | } 186 | } 187 | case e => { 188 | count(4000).flatMap { _ => 189 | implicitly[MonadThrowable[Script]].raiseError(e) 190 | } 191 | } 192 | } 193 | } 194 | 195 | } 196 | 197 | private def newScript: Script[Int] = { 198 | throwableMonadic[Script] { 199 | count(50000).each 200 | try { 201 | if (randomInt.each > 100) { 202 | count(600000).each 203 | throw MyException 204 | count(7000000).each 205 | 789 206 | } else if (randomInt.each > 10) { 207 | count(1).each 208 | 123 209 | } else { 210 | count(20).each 211 | (throw new IOException): Int 212 | } 213 | } catch { 214 | case e: IOException => { 215 | count(300).each 216 | 456 217 | } 218 | } finally { 219 | count(4000).each 220 | } 221 | } 222 | } 223 | 224 | @Test 225 | def testFreeMyException(): Unit = { 226 | val script = newScript 227 | var count = 0 228 | val result: Throwable \/ Int = script.run.foldMap(new (Command ~> Id.Id) { 229 | override def apply[A](command: Command[A]): A = { 230 | command match { 231 | case Count(delta) => { 232 | count += delta 233 | \/-(count) 234 | } 235 | case RandomInt => { 236 | -\/(MyException) 237 | } 238 | } 239 | } 240 | }) 241 | Assert.assertEquals(\/.fromTryCatchNonFatal(noScript(() => throw MyException)), result) 242 | Assert.assertEquals(-\/(MyException), result) 243 | Assert.assertEquals(54000, count) 244 | } 245 | 246 | @Test 247 | def testFreeIOException(): Unit = { 248 | val script = newScript 249 | var count = 0 250 | val result: Throwable \/ Int = script.run.foldMap(new (Command ~> Id.Id) { 251 | override def apply[A](command: Command[A]): A = { 252 | command match { 253 | case Count(delta) => { 254 | count += delta 255 | \/-(count) 256 | } 257 | case RandomInt => { 258 | -\/(new IOException) 259 | } 260 | } 261 | } 262 | }) 263 | Assert.assertEquals(\/.fromTryCatchNonFatal(noScript(() => throw new IOException)), result) 264 | Assert.assertEquals(\/-(456), result) 265 | Assert.assertEquals(54300, count) 266 | } 267 | 268 | @Test 269 | def testFree150(): Unit = { 270 | val script = newScript 271 | var count = 0 272 | val result: Throwable \/ Int = script.run.foldMap(new (Command ~> Id.Id) { 273 | override def apply[A](command: Command[A]): A = { 274 | command match { 275 | case Count(delta) => { 276 | count += delta 277 | \/-(count) 278 | } 279 | case RandomInt => { 280 | \/-(150) 281 | } 282 | } 283 | } 284 | }) 285 | Assert.assertEquals(\/.fromTryCatchNonFatal(noScript(() => 150)), result) 286 | Assert.assertEquals(-\/(MyException), result) 287 | Assert.assertEquals(654000, count) 288 | } 289 | 290 | @Test 291 | def testFree15(): Unit = { 292 | val script = newScript 293 | var count = 0 294 | val result: Throwable \/ Int = script.run.foldMap(new (Command ~> Id.Id) { 295 | override def apply[A](command: Command[A]): A = { 296 | command match { 297 | case Count(delta) => { 298 | count += delta 299 | \/-(count) 300 | } 301 | case RandomInt => { 302 | \/-(15) 303 | } 304 | } 305 | } 306 | }) 307 | Assert.assertEquals(\/.fromTryCatchNonFatal(noScript(() => 15)), result) 308 | Assert.assertEquals(\/-(123), result) 309 | Assert.assertEquals(54001, count) 310 | } 311 | 312 | @Test 313 | def testFree5(): Unit = { 314 | val script = newScript 315 | var count = 0 316 | val result: Throwable \/ Int = script.run.foldMap(new (Command ~> Id.Id) { 317 | override def apply[A](command: Command[A]): A = { 318 | command match { 319 | case Count(delta) => { 320 | count += delta 321 | \/-(count) 322 | } 323 | case RandomInt => { 324 | \/-(5) 325 | } 326 | } 327 | } 328 | }) 329 | Assert.assertEquals(\/.fromTryCatchNonFatal(noScript(() => 5)), result) 330 | Assert.assertEquals(\/-(456), result) 331 | Assert.assertEquals(54320, count) 332 | } 333 | 334 | } 335 | -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/MonadicTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 ThoughtWorks, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.thoughtworks.each 18 | 19 | import com.thoughtworks.each.ComprehensionImplicits._ 20 | import com.thoughtworks.each.Monadic._ 21 | import org.junit.{Assert, Test} 22 | 23 | import scala.concurrent.duration.Duration 24 | import scala.concurrent.{Await, Future} 25 | import scalaz.{IndexedStateT, Monad} 26 | 27 | import scalaz.effect.IO 28 | 29 | class MonadicTest { 30 | 31 | @Test 32 | def testOption(): Unit = { 33 | def plusOne(intOption: Option[Int]) = monadic[Option] { 34 | intOption.each + 1 35 | } 36 | Assert.assertEquals(None, plusOne(None)) 37 | Assert.assertEquals(Some(16), plusOne(Some(15))) 38 | } 39 | 40 | @Test 41 | def testSeq(): Unit = { 42 | def plusOne(intSeq: Seq[Int]) = monadic[Seq] { 43 | intSeq.each + 1 44 | } 45 | Assert.assertEquals(Seq.empty, plusOne(Seq.empty)) 46 | Assert.assertEquals(Seq(16), plusOne(Seq(15))) 47 | Assert.assertEquals(Seq(16, -1, 10), plusOne(Seq(15, -2, 9))) 48 | } 49 | 50 | @Test 51 | def testFuture(): Unit = { 52 | import scala.concurrent.ExecutionContext.Implicits.global 53 | val f101 = monadic[Future] { 54 | Future(1).each + Future(100).each 55 | } 56 | Assert.assertEquals(101, Await.result(f101, Duration.Inf)) 57 | } 58 | 59 | @Test 60 | def testPow(): Unit = { 61 | val pow = monadic[Seq](math.pow(2.0, (0 to 10).each)) 62 | Assert.assertEquals(Seq(1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0), pow) 63 | } 64 | 65 | @Test 66 | def testMultiply(): Unit = { 67 | val result = monadic[Seq]((0 to 3).each * (10 to 13).each) 68 | Assert.assertEquals(Seq(0, 0, 0, 0, 10, 11, 12, 13, 20, 22, 24, 26, 30, 33, 36, 39), result) 69 | } 70 | 71 | @Test 72 | def testIoWhile(): Unit = { 73 | 74 | def s = IO("123") 75 | var count = 0 76 | val io = monadic[IO] { 77 | var i = 0 78 | while (i < 100) { 79 | count += s.each.length 80 | i += 1 81 | } 82 | i 83 | } 84 | 85 | Assert.assertEquals(100, io.unsafePerformIO()) 86 | 87 | Assert.assertEquals(300, count) 88 | } 89 | 90 | @Test 91 | def testWhile(): Unit = { 92 | def s = Option("123") 93 | var count = 0 94 | monadic[Option] { 95 | val i = 100 96 | while (i != 100) { 97 | count += s.each.length 98 | } 99 | } 100 | Assert.assertEquals(0, count) 101 | } 102 | 103 | 104 | @Test 105 | def testIf(): Unit = { 106 | val ifOption = monadic[Option] { 107 | val i = Option(1) 108 | val j: Int = if (i.each > 1) 2 else 10 109 | i.each + j 110 | } 111 | 112 | Assert.assertEquals(Some(11), ifOption) 113 | } 114 | 115 | 116 | @Test 117 | def testReturn(): Unit = { 118 | 119 | def returnExprssions(input: Option[Int]): Option[Int] = monadic[Option] { 120 | if (input.each < 0) { 121 | return Some(-1) 122 | } 123 | if (input.each < 10) { 124 | return Some(0) 125 | } 126 | input.each 127 | } 128 | 129 | Assert.assertEquals(Some(-1), returnExprssions(Some(-1234))) 130 | Assert.assertEquals(Some(0), returnExprssions(Some(5))) 131 | Assert.assertEquals(Some(13), returnExprssions(Some(13))) 132 | Assert.assertEquals(None, returnExprssions(None)) 133 | } 134 | 135 | @Test 136 | def testImport(): Unit = { 137 | object A { 138 | def currentImport = "A" 139 | } 140 | 141 | object B { 142 | def currentImport = "B" 143 | } 144 | 145 | object C { 146 | def currentImport = "C" 147 | } 148 | 149 | val result = monadic[Option] { 150 | import A._ 151 | Assert.assertEquals("A", currentImport) 152 | 153 | { 154 | import B._ 155 | Assert.assertEquals("B", currentImport) 156 | 157 | { 158 | import C._ 159 | Assert.assertEquals("C", currentImport) 160 | } 161 | } 162 | 163 | currentImport 164 | } 165 | 166 | Assert.assertEquals(Some("A"), result) 167 | } 168 | 169 | @Test 170 | def testAssignExpressions(): Unit = { 171 | val assignExp = monadic[Option] { 172 | var pi = 3.1415 173 | pi = 1.0 174 | pi 175 | } 176 | 177 | Assert.assertEquals(Some(1.0), assignExp) 178 | 179 | } 180 | 181 | @Test 182 | def testDefDef(): Unit = { 183 | 184 | val lengthOption = monadic[Option] { 185 | def s = Option(Nil) 186 | s.each.length 187 | } 188 | 189 | Assert.assertEquals(Monad[Option].map { 190 | def s = Option(Nil) 191 | s 192 | }(_.length), lengthOption) 193 | } 194 | 195 | @Test 196 | def testSomeNilLength(): Unit = { 197 | val s = Option(Nil) 198 | 199 | val lengthOption = monadic[Option] { 200 | s.each.length 201 | } 202 | 203 | Assert.assertEquals(Monad[Option].map(s)(_.length), lengthOption) 204 | 205 | } 206 | 207 | @Test 208 | def testNoneLength(): Unit = { 209 | val s: Option[Seq[Nothing]] = None 210 | 211 | val lengthOption = monadic[Option] { 212 | s.each.length 213 | } 214 | 215 | Assert.assertEquals(Monad[Option].map(s)(_.length), lengthOption) 216 | 217 | } 218 | 219 | @Test 220 | def testNewByOption(): Unit = { 221 | val newS = monadic[Option] { 222 | new String("a string") 223 | } 224 | 225 | Assert.assertEquals(Monad[Option].pure(new String("a string")), newS) 226 | Assert.assertEquals(Some(new String("a string")), newS) 227 | } 228 | 229 | @Test 230 | def testNewBySeq(): Unit = { 231 | val newS = monadic[Seq] { 232 | new String("a string") 233 | } 234 | 235 | Assert.assertEquals(Monad[Seq].pure(new String("a string")), newS) 236 | Assert.assertEquals(Seq(new String("a string")), newS) 237 | } 238 | 239 | @Test 240 | def testConcatSeq = { 241 | 242 | val list1 = Seq("foo", "bar", "baz") 243 | val list2 = Seq("Hello", "World!") 244 | 245 | val concatSeq = monadic[Seq](list1.each.substring(0, 2) + " " + list2.each.substring(1, 4)) 246 | 247 | Assert.assertEquals( 248 | for { 249 | string1 <- list1 250 | string2 <- list2 251 | } yield (string1.substring(0, 2) + " " + string2.substring(1, 4)), 252 | concatSeq) 253 | Assert.assertEquals(Seq("fo ell", "fo orl", "ba ell", "ba orl", "ba ell", "ba orl"), concatSeq) 254 | } 255 | 256 | @Test 257 | def testConcatSet = { 258 | 259 | val list1 = Set("foo", "bar", "baz") 260 | val list2 = Set("Hello", "World!") 261 | 262 | val concatSet = monadic[Set](list1.each.substring(0, 2) + " " + list2.each.substring(1, 4)) 263 | 264 | Assert.assertEquals( 265 | for { 266 | string1 <- list1 267 | string2 <- list2 268 | } yield (string1.substring(0, 2) + " " + string2.substring(1, 4)), 269 | concatSet) 270 | Assert.assertEquals(Set("fo ell", "fo orl", "ba ell", "ba orl", "ba ell", "ba orl"), concatSet) 271 | } 272 | 273 | @Test 274 | def testBlock(): Unit = { 275 | var count = 0 276 | val io = monadic[IO] { 277 | val _ = IO(()).each 278 | count += 1 279 | count += 1 280 | count 281 | } 282 | Assert.assertEquals(0, count) 283 | Assert.assertEquals(2, io.unsafePerformIO()) 284 | Assert.assertEquals(2, count) 285 | 286 | } 287 | 288 | @Test 289 | def testCatch(): Unit = { 290 | var count = 0 291 | val io = catchIoMonadic[IO] { 292 | val _ = IO(()).each 293 | try { 294 | count += 1 295 | (null: Array[Int])(0) 296 | } catch { 297 | case e: NullPointerException => { 298 | count += 1 299 | 100 300 | } 301 | } finally { 302 | count += 1 303 | } 304 | } 305 | Assert.assertEquals(0, count) 306 | Assert.assertEquals(100, io.unsafePerformIO()) 307 | Assert.assertEquals(3, count) 308 | } 309 | 310 | @Test 311 | def testThrowCatch(): Unit = { 312 | var count = 0 313 | val io = catchIoMonadic[IO] { 314 | val _ = IO(()).each 315 | try { 316 | count += 1 317 | throw new Exception 318 | } catch { 319 | case e: Exception => { 320 | count += 1 321 | // FIXME: compile time error on Scala 2.13 if removing `: Int` 322 | 100: Int 323 | } 324 | } finally { 325 | count += 1 326 | } 327 | } 328 | Assert.assertEquals(0, count) 329 | Assert.assertEquals(100, io.unsafePerformIO()) 330 | Assert.assertEquals(3, count) 331 | } 332 | 333 | @Test 334 | def testNestedClass(): Unit = { 335 | trait Base { 336 | def bar: Int 337 | } 338 | val nestedClass = monadic[Option][Base] { 339 | class Foo() extends Base { 340 | def bar = 100 341 | } 342 | new Foo 343 | } 344 | 345 | Assert.assertEquals(100, nestedClass.get.bar) 346 | } 347 | 348 | @Test 349 | def testVarIf(): Unit = { 350 | var count = 0 351 | def io(initialValue: Int) = monadic[IO] { 352 | var i = initialValue 353 | if (i == 0) { 354 | i = 1 355 | } else { 356 | i = 2 357 | } 358 | i += 10 359 | i 360 | } 361 | 362 | Assert.assertEquals(11, io(0).unsafePerformIO()) 363 | Assert.assertEquals(12, io(-1).unsafePerformIO()) 364 | 365 | val state = { 366 | IndexedStateT.stateTMonadState[Int, IO].ifM( 367 | IndexedStateT.stateTMonadState[Int, IO].get.map(_ == 0), 368 | IndexedStateT.stateTMonadState[Int, IO].put(1), 369 | IndexedStateT.stateTMonadState[Int, IO].put(2) 370 | ).flatMap { _ => 371 | IndexedStateT.stateTMonadState[Int, IO].get 372 | }.flatMap { v => 373 | IndexedStateT.stateTMonadState[Int, IO].put(v + 10) 374 | }.flatMap { _ => 375 | IndexedStateT.stateTMonadState[Int, IO].get 376 | } 377 | } 378 | 379 | Assert.assertEquals(state.eval(0).unsafePerformIO(), io(0).unsafePerformIO()) 380 | Assert.assertEquals(state.eval(-1).unsafePerformIO(), io(-1).unsafePerformIO()) 381 | } 382 | 383 | @Test 384 | def testMatch(): Unit = { 385 | 386 | val optionHead = monadic[Option] { 387 | (Option(Seq("foo", "bar", "baz")).each match { 388 | case head :: tail => { 389 | Some(head) 390 | } 391 | case _ => { 392 | None 393 | } 394 | }).each 395 | } 396 | 397 | Assert.assertEquals(Some("foo"), optionHead) 398 | } 399 | 400 | @Test 401 | def testIoDoWhile(): Unit = { 402 | def s = IO("123") 403 | var count = 0 404 | val io = monadic[IO] { 405 | var i = 0 406 | do { 407 | count += s.each.length 408 | i += 1 409 | } while (i < 100) 410 | i 411 | } 412 | 413 | Assert.assertEquals(100, io.unsafePerformIO()) 414 | 415 | Assert.assertEquals(300, count) 416 | } 417 | 418 | 419 | @Test 420 | def testDoWhile(): Unit = { 421 | def s = Option("123") 422 | var count = 0 423 | val option = monadic[Option] { 424 | var i = 0 425 | do { 426 | count += s.each.length 427 | i += 1 428 | } while (i < 0) 429 | i 430 | } 431 | 432 | Assert.assertEquals(Some(1), option) 433 | 434 | Assert.assertEquals(3, count) 435 | } 436 | 437 | @Test 438 | def testThis(): Unit = { 439 | import scala.language.existentials 440 | val thisClass = monadic[Option] { 441 | this.getClass.asInstanceOf[Class[MonadicTest]] 442 | } 443 | 444 | Assert.assertEquals(Some(classOf[MonadicTest]), thisClass) 445 | } 446 | 447 | @Test 448 | def testTuple(): Unit = { 449 | val result = monadic[Option] { 450 | val (a, b, c) = Some((1, 2, 3)).each 451 | a + b + c 452 | } 453 | 454 | Assert.assertEquals(Some(6), result) 455 | } 456 | 457 | @Test 458 | def testSuper(): Unit = { 459 | class Super { 460 | def foo = "super" 461 | } 462 | 463 | object Child extends Super { 464 | override def foo = "child" 465 | 466 | val superFoo = monadic[Option] { 467 | super.foo 468 | } 469 | } 470 | 471 | Assert.assertEquals(Some("super"), Child.superFoo) 472 | } 473 | 474 | @Test 475 | def testAnnotation(): Unit = { 476 | val selector = Seq(1, 2, 3) 477 | Assert.assertEquals(Some(Seq(1, 2, 3)), monadic[Option] { 478 | (selector: @unchecked) match { 479 | case s: Seq[String@unchecked] => { 480 | s 481 | } 482 | } 483 | }) 484 | } 485 | 486 | @Test 487 | def testXml(): Unit = { 488 | val someFoo = Option() 489 | val result = monadic[Option] { 490 | 491 | {someFoo.each} 492 | 493 | } 494 | Assert.assertEquals(Some( 495 | 496 | 497 | ), result) 498 | } 499 | } 500 | 501 | -------------------------------------------------------------------------------- /each/src/test/scala/com/thoughtworks/each/TraverseComprehensionTest.scala: -------------------------------------------------------------------------------- 1 | package com.thoughtworks.each 2 | 3 | import org.junit.{Assert, Test} 4 | import Monadic._ 5 | import scalaz.std.option._ 6 | import scalaz.std.list._ 7 | 8 | class TraverseComprehensionTest { 9 | 10 | @Test 11 | def testAnnotationForeach(): Unit = { 12 | import scalaz.std.iterable._ 13 | val n = Some(10) 14 | @monadic[Option] 15 | val result = { 16 | var count = 1 17 | for (i <- 1 to 10) { 18 | count += i * n.each 19 | } 20 | count 21 | } 22 | Assert.assertEquals(Some(551), result) 23 | } 24 | 25 | @Test 26 | def testForeach(): Unit = { 27 | val n = Some(10) 28 | val result = monadic[Option] { 29 | var count = 1 30 | for (i <- List(300, 20).monadicLoop) { 31 | count += i * n.each 32 | } 33 | count 34 | } 35 | Assert.assertEquals(Some(3201), result) 36 | } 37 | 38 | @Test 39 | def testMap(): Unit = { 40 | val n = Some(4000) 41 | val result = monadic[Option] { 42 | (for (i <- List(300, 20).monadicLoop) yield { 43 | i + n.each 44 | }).underlying 45 | } 46 | Assert.assertEquals(Some(List(4300, 4020)), result) 47 | } 48 | 49 | 50 | @Test 51 | def testMapWithAnotherType(): Unit = { 52 | val result = monadic[Option] { 53 | (for (i <- List("foo", "bar-baz").monadicLoop) yield { 54 | i.length 55 | }).underlying 56 | } 57 | Assert.assertEquals(Some(List(3, 7)), result) 58 | } 59 | 60 | @Test 61 | def testFlatMap(): Unit = { 62 | val n = Some(4000) 63 | val result = monadic[Option] { 64 | (for { 65 | i <- List(300, 20).monadicLoop 66 | j <- List(50000, 600000).monadicLoop 67 | } yield { 68 | i + j + n.each 69 | }).underlying 70 | } 71 | Assert.assertEquals(Some(List(54300, 604300, 54020, 604020)), result) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.8.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin( 2 | "com.thoughtworks.sbt-best-practice" % "sbt-best-practice" % "8.2.5" 3 | ) 4 | 5 | addSbtPlugin("com.thoughtworks.sbt-scala-js-map" % "sbt-scala-js-map" % "4.0.0") 6 | 7 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.33") 8 | 9 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.17") 10 | 11 | addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") 12 | 13 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") 14 | 15 | addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.1.2") 16 | 17 | addSbtPlugin("com.thoughtworks.example" % "sbt-example" % "9.2.1") 18 | 19 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") 20 | -------------------------------------------------------------------------------- /project/plugins.sbt.scala-js.1.x: -------------------------------------------------------------------------------- 1 | // An optional sbt file to replace Scala.js 1.0 with 0.6 2 | dependencyOverrides += Defaults.sbtPluginExtra( 3 | "org.scala-js" % "sbt-scalajs" % "1.0.1", 4 | sbtBinaryVersion.value, 5 | scalaBinaryVersion.value, 6 | ) 7 | 8 | Compile / sourceGenerators += Def.task { 9 | val file = (Compile / sourceManaged).value / "SkipPublishForNonScalaJSProjects.scala" 10 | IO.write(file, """ 11 | import scalajscrossproject.ScalaJSCrossPlugin.autoImport._ 12 | import sbtcrossproject.CrossPlugin.autoImport._ 13 | import sbt._, Keys._ 14 | object SkipPublishForNonScalaJSProjects extends AutoPlugin { 15 | override def trigger = allRequirements 16 | override def projectSettings = Seq( 17 | publish / skip := crossProjectPlatform.value != JSPlatform 18 | ) 19 | } 20 | """) 21 | Seq(file) 22 | }.taskValue 23 | -------------------------------------------------------------------------------- /project/sonatypeResolver.sbt: -------------------------------------------------------------------------------- 1 | resolvers in ThisBuild += "Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases" 2 | -------------------------------------------------------------------------------- /secret.sbt: -------------------------------------------------------------------------------- 1 | lazy val secret = { 2 | for (token <- sys.env.get("GITHUB_PERSONAL_ACCESS_TOKEN")) yield { 3 | val secret = project.settings(publish / skip := true).in { 4 | val secretDirectory = file(sourcecode.File()).getParentFile / "secret" 5 | IO.delete(secretDirectory) 6 | org.eclipse.jgit.api.Git 7 | .cloneRepository() 8 | .setURI( 9 | "https://github.com/ThoughtWorksInc/tw-data-china-continuous-delivery-password.git" 10 | ) 11 | .setDirectory(secretDirectory) 12 | .setCredentialsProvider( 13 | new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider( 14 | token, 15 | "" 16 | ) 17 | ) 18 | .call() 19 | .close() 20 | secretDirectory 21 | } 22 | secret 23 | } 24 | }.getOrElse(null) 25 | -------------------------------------------------------------------------------- /sonatypeResolver.sbt: -------------------------------------------------------------------------------- 1 | resolvers in ThisBuild ++= Seq(Opts.resolver.sonatypeSnapshots, Opts.resolver.sonatypeStaging) 2 | --------------------------------------------------------------------------------