├── .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 | [](https://gitter.im/ThoughtWorksInc/each?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/ThoughtWorksInc/each)
5 | [](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 |
46 | {toScript(GetContactNameByEmail(email)).each} 47 | | 48 |49 | {email} 50 | | 51 |