├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── core ├── jvm │ └── src │ │ └── test │ │ └── scala-2 │ │ └── org │ │ └── typelevel │ │ └── twiddles │ │ └── test │ │ └── CompilationPerformanceSpec.scala └── shared │ └── src │ ├── main │ ├── scala-2 │ │ └── org │ │ │ └── typelevel │ │ │ └── twiddles │ │ │ ├── DropUnits.scala │ │ │ ├── Iso.scala │ │ │ ├── TupleOps.scala │ │ │ ├── TwiddleCompat.scala │ │ │ ├── TwiddleOpDropUnits.scala │ │ │ ├── Twiddles.scala │ │ │ └── package.scala │ └── scala-3 │ │ └── org │ │ └── typelevel │ │ └── twiddles │ │ ├── DropUnits.scala │ │ ├── Iso.scala │ │ ├── TwiddleCompat.scala │ │ └── Twiddles.scala │ └── test │ └── scala │ └── examples │ ├── Example.scala │ └── Hierarchy.scala ├── docs └── src │ └── README.md └── project ├── build.properties └── plugins.sbt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**', '!update/**', '!pr/**'] 13 | push: 14 | branches: ['**', '!update/**', '!pr/**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | 21 | concurrency: 22 | group: ${{ github.workflow }} @ ${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | build: 27 | name: Test 28 | strategy: 29 | matrix: 30 | os: [ubuntu-22.04] 31 | scala: [3, 2.12, 2.13] 32 | java: [temurin@8] 33 | project: [rootJS, rootJVM, rootNative] 34 | runs-on: ${{ matrix.os }} 35 | timeout-minutes: 60 36 | steps: 37 | - name: Checkout current branch (full) 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Setup sbt 43 | uses: sbt/setup-sbt@v1 44 | 45 | - name: Setup Java (temurin@8) 46 | id: setup-java-temurin-8 47 | if: matrix.java == 'temurin@8' 48 | uses: actions/setup-java@v4 49 | with: 50 | distribution: temurin 51 | java-version: 8 52 | cache: sbt 53 | 54 | - name: sbt update 55 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 56 | run: sbt +update 57 | 58 | - name: Check that workflows are up to date 59 | run: sbt githubWorkflowCheck 60 | 61 | - name: Check headers and formatting 62 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 63 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck 64 | 65 | - name: scalaJSLink 66 | if: matrix.project == 'rootJS' 67 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult 68 | 69 | - name: nativeLink 70 | if: matrix.project == 'rootNative' 71 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink 72 | 73 | - name: Test 74 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test 75 | 76 | - name: Check binary compatibility 77 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 78 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues 79 | 80 | - name: Generate API documentation 81 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 82 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc 83 | 84 | - name: Make target directories 85 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 86 | run: mkdir -p core/native/target core/js/target core/jvm/target project/target 87 | 88 | - name: Compress target directories 89 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 90 | run: tar cf targets.tar core/native/target core/js/target core/jvm/target project/target 91 | 92 | - name: Upload target directories 93 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} 97 | path: targets.tar 98 | 99 | publish: 100 | name: Publish Artifacts 101 | needs: [build] 102 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 103 | strategy: 104 | matrix: 105 | os: [ubuntu-22.04] 106 | java: [temurin@8] 107 | runs-on: ${{ matrix.os }} 108 | steps: 109 | - name: Checkout current branch (full) 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | 114 | - name: Setup sbt 115 | uses: sbt/setup-sbt@v1 116 | 117 | - name: Setup Java (temurin@8) 118 | id: setup-java-temurin-8 119 | if: matrix.java == 'temurin@8' 120 | uses: actions/setup-java@v4 121 | with: 122 | distribution: temurin 123 | java-version: 8 124 | cache: sbt 125 | 126 | - name: sbt update 127 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 128 | run: sbt +update 129 | 130 | - name: Download target directories (3, rootJS) 131 | uses: actions/download-artifact@v4 132 | with: 133 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS 134 | 135 | - name: Inflate target directories (3, rootJS) 136 | run: | 137 | tar xf targets.tar 138 | rm targets.tar 139 | 140 | - name: Download target directories (3, rootJVM) 141 | uses: actions/download-artifact@v4 142 | with: 143 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM 144 | 145 | - name: Inflate target directories (3, rootJVM) 146 | run: | 147 | tar xf targets.tar 148 | rm targets.tar 149 | 150 | - name: Download target directories (3, rootNative) 151 | uses: actions/download-artifact@v4 152 | with: 153 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative 154 | 155 | - name: Inflate target directories (3, rootNative) 156 | run: | 157 | tar xf targets.tar 158 | rm targets.tar 159 | 160 | - name: Download target directories (2.12, rootJS) 161 | uses: actions/download-artifact@v4 162 | with: 163 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJS 164 | 165 | - name: Inflate target directories (2.12, rootJS) 166 | run: | 167 | tar xf targets.tar 168 | rm targets.tar 169 | 170 | - name: Download target directories (2.12, rootJVM) 171 | uses: actions/download-artifact@v4 172 | with: 173 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJVM 174 | 175 | - name: Inflate target directories (2.12, rootJVM) 176 | run: | 177 | tar xf targets.tar 178 | rm targets.tar 179 | 180 | - name: Download target directories (2.12, rootNative) 181 | uses: actions/download-artifact@v4 182 | with: 183 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootNative 184 | 185 | - name: Inflate target directories (2.12, rootNative) 186 | run: | 187 | tar xf targets.tar 188 | rm targets.tar 189 | 190 | - name: Download target directories (2.13, rootJS) 191 | uses: actions/download-artifact@v4 192 | with: 193 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS 194 | 195 | - name: Inflate target directories (2.13, rootJS) 196 | run: | 197 | tar xf targets.tar 198 | rm targets.tar 199 | 200 | - name: Download target directories (2.13, rootJVM) 201 | uses: actions/download-artifact@v4 202 | with: 203 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJVM 204 | 205 | - name: Inflate target directories (2.13, rootJVM) 206 | run: | 207 | tar xf targets.tar 208 | rm targets.tar 209 | 210 | - name: Download target directories (2.13, rootNative) 211 | uses: actions/download-artifact@v4 212 | with: 213 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootNative 214 | 215 | - name: Inflate target directories (2.13, rootNative) 216 | run: | 217 | tar xf targets.tar 218 | rm targets.tar 219 | 220 | - name: Import signing key 221 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' 222 | env: 223 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 224 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 225 | run: echo $PGP_SECRET | base64 -d -i - | gpg --import 226 | 227 | - name: Import signing key and strip passphrase 228 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' 229 | env: 230 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 231 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 232 | run: | 233 | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg 234 | echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg 235 | (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) 236 | 237 | - name: Publish 238 | env: 239 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 240 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 241 | SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} 242 | run: sbt tlCiRelease 243 | 244 | dependency-submission: 245 | name: Submit Dependencies 246 | if: github.event.repository.fork == false && github.event_name != 'pull_request' 247 | strategy: 248 | matrix: 249 | os: [ubuntu-22.04] 250 | java: [temurin@8] 251 | runs-on: ${{ matrix.os }} 252 | steps: 253 | - name: Checkout current branch (full) 254 | uses: actions/checkout@v4 255 | with: 256 | fetch-depth: 0 257 | 258 | - name: Setup sbt 259 | uses: sbt/setup-sbt@v1 260 | 261 | - name: Setup Java (temurin@8) 262 | id: setup-java-temurin-8 263 | if: matrix.java == 'temurin@8' 264 | uses: actions/setup-java@v4 265 | with: 266 | distribution: temurin 267 | java-version: 8 268 | cache: sbt 269 | 270 | - name: sbt update 271 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 272 | run: sbt +update 273 | 274 | - name: Submit Dependencies 275 | uses: scalacenter/sbt-dependency-submission@v2 276 | with: 277 | modules-ignore: rootjs_3 rootjs_2.12 rootjs_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 278 | configs-ignore: test scala-tool scala-doc-tool test-internal 279 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .bloop 4 | .bsp 5 | target 6 | metals.sbt 7 | .vscode 8 | .metals 9 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.8.6" 2 | 3 | style = default 4 | 5 | runner.dialect = scala3 6 | 7 | maxColumn = 100 8 | 9 | docstrings.wrap = "no" 10 | 11 | rewrite.rules = [ 12 | AvoidInfix 13 | RedundantBraces 14 | RedundantParens 15 | Imports 16 | ] 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Typelevel 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Code in Twiddles is derived in part from scodec. The scodec license follows: 2 | 3 | Copyright (c) 2013-2014, Michael Pilquist and Paul Chiusano 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 3. Neither the name of the scodec team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twiddles 2 | 3 | A twiddle list is a list of one or more values, potentially of differing types, that supports incremental creation and supports conversion to case classes that are "shape compatible" with the constituent types of the twiddle list. 4 | 5 | Twiddle lists are useful in the creation of protocols (e.g., decoders, encoders, codecs), where a protocol for a complex type is built from simpler constituent protocols. This technique was first popularized by parser combinators with syntax like `lparen ~ expr ~ rparen`. In contrast to type driven derivation schemes, where protocols are implicitly determined by the constituent types of a data constructor, twiddle lists keep the focus on the protocol. 6 | 7 | This library provides the ability to work with twiddle lists for arbitrary types and provides a single API that works for both Scala 3 and Scala 2. On Scala 3, twiddle lists are represented as generic tuples -- e.g., `F[Int *: String *: Boolean *: EmptyTuple]` or equivalently `F[(Int, String, Boolean)]`. On Scala 2, twiddle lists are represented as Shapeless heterogeneous lists. The `org.typelevel.twiddles` package provides type aliases that allow for source compatibility (`*:` is aliased to `shapeles.::` and `EmptyTuple` is aliased to `shapeless.HNil`). 8 | 9 | ## Getting Started 10 | 11 | Artifacts are published for Scala 2.12, 2.13, and 3 and all platforms (JVM, Scala.js, and Scala Native). 12 | 13 | ```scala 14 | libraryDependencies += "org.typelevel" %%% "twiddles-core" % "0.7.2" // check Releases for the latest version 15 | ``` 16 | 17 | ```scala 18 | // Enable twiddle syntax for arbitrary types 19 | import org.typelevel.twiddles.syntax._ 20 | 21 | case class Foo(x: Int, y: String) 22 | 23 | val a = Option(42) 24 | // a: Option[Int] = Some(value = 42) 25 | val b = Option("Hi") 26 | // b: Option[String] = Some(value = "Hi") 27 | 28 | val foo = (a *: b).to[Foo] 29 | // foo: Option[Foo] = Some(value = Foo(x = 42, y = "Hi")) 30 | ``` 31 | 32 | In this example, `a *: b` creates an `Option[Int *: String *: EmptyTuple]`. We then convert that value to an `Option[Foo]` via `.to[Foo]`. 33 | 34 | The `*:` operation comes from the imported twiddle syntax and is similar to the Scala 3 built-in tuple cons operation, but works on applied type constructors. The expression `a *: b *: c` builds an `F[A *: B *: C *: EmptyTuple]` from an `F[A]`, `F[B]`, and `F[C]`. The `*:` operation requires that the type constructor `F` has a `cats.InvariantSemigroupal` instance. 35 | 36 | The `to` operation also comes from the imported twiddle syntax. Calling `.to[X]` on an `F[T]` for some twiddle list `T` results in an `F[X]` provided that `T` is shape compatible with `X`. In the most common case where `X` is a case class, shape compatibility is defined as `T` having the same types in the same order as the parameters of `X`. The `to` operation requires that the type constructor `F` has a `cats.Invariant` instance. 37 | 38 | Invariant semigroupals are much more general than (covariant) functors, which means twiddle list support works for a wide variety of data types. For instance, contravariant functors are invariant semigroupals allowing us to use twiddle list syntax to incrementally build instances: 39 | 40 | ```scala 41 | val fooOrdering = (summon[Ordering[Int]] *: summon[Ordering[String]]).to[Foo] 42 | // fooOrdering: Ordering[Foo] = scala.math.Ordering$$anon$1@14de39e3 43 | ``` 44 | 45 | ## Library Usage 46 | 47 | When designing a library that uses twiddle lists, the `TwiddleSyntax` trait can be mixed in to the companion object of a type constructor. This has the effect of providing twiddle syntax without requiring users of the library to import `org.typelevel.twiddles.syntax._` at each call site. 48 | 49 | ```scala 50 | import org.typelevel.twiddles.TwiddleSyntax 51 | import cats.Applicative 52 | 53 | trait Json 54 | trait Decoder[A] { 55 | def decode(j: Json): Option[A] 56 | } 57 | object Decoder extends TwiddleSyntax[Decoder] { 58 | implicit val applicative: Applicative[Decoder] = new Applicative[Decoder] { 59 | def pure[A](a: A): Decoder[A] = _ => Some(a) 60 | def ap[A, B](ff: Decoder[A => B])(fa: Decoder[A]): Decoder[B] = j => 61 | for { 62 | f <- ff.decode(j) 63 | a <- fa.decode(j) 64 | } yield f(a) 65 | } 66 | } 67 | 68 | val int: Decoder[Int] = _ => ??? 69 | // int: Decoder[Int] = repl.MdocSession$MdocApp0$$Lambda$9134/0x0000000802278000@3992fe1c 70 | val string: Decoder[String] = _ => ??? 71 | // string: Decoder[String] = repl.MdocSession$MdocApp0$$Lambda$9135/0x0000000802278448@626b82b2 72 | 73 | case class Foo(x: Int, y: String) 74 | val fooDecoder = (int *: string).to[Foo] 75 | // fooDecoder: Decoder[Foo] = repl.MdocSession$$anon$8$$Lambda$9138/0x0000000802279308@63c9be6e 76 | ``` 77 | 78 | In this example, the `Decoder` type has an `Applicative` instance defined in its companion object (and `Applicative` extends `InvariantSemigroupal`), and the companion object extends `TwiddleSyntax`. The latter enables use of `*:` and `to` with `Decoder` values without adding explicit imports (that is, there's no need to import `org.typelevel.twiddles.syntax._` at call sites). 79 | 80 | ## Etymology 81 | 82 | The term "twiddle list" was first coined by [Rob Norris](https://github.com/tpolecat) in the [Skunk](https://github.com/tpolecat/skunk) library, where a twiddle list was defined as a left nested tuple. For example, a 4 element twiddle list consisting of an `Int`, `String`, `Boolean`, and `Double` was represented as `(((Int, String), Boolean), Double)`. 83 | 84 | This library uses a different encoding -- twiddle lists are encoded as tuples on Scala 3 and Shapeless heterogeneous lists on Scala 2. The previous 4 element twiddle list is represented as `Int *: String *: Boolean *: Double *: EmptyTuple`. 85 | 86 | We adopt the name "twiddle list" to refer to the general technique of incremental construction of complex protocols. 87 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.tools.mima.core._ 2 | 3 | Global / onChangedBuildSource := ReloadOnSourceChanges 4 | 5 | ThisBuild / tlBaseVersion := "0.9" 6 | 7 | ThisBuild / organization := "org.typelevel" 8 | ThisBuild / organizationName := "Typelevel" 9 | ThisBuild / startYear := Some(2023) 10 | 11 | ThisBuild / crossScalaVersions := Seq("3.3.5", "2.12.20", "2.13.16") 12 | ThisBuild / scalaVersion := (ThisBuild / crossScalaVersions).value.head 13 | 14 | ThisBuild / doctestTestFramework := DoctestTestFramework.ScalaCheck 15 | 16 | ThisBuild / developers ++= List( 17 | "mpilquist" -> "Michael Pilquist" 18 | ).map { case (username, fullName) => 19 | tlGitHubDev(username, fullName) 20 | } 21 | 22 | ThisBuild / licenses := List( 23 | ("BSD-3-Clause", url("https://github.com/typelevel/twiddles/blob/main/LICENSE")) 24 | ) 25 | 26 | ThisBuild / mimaBinaryIssueFilters ++= Seq( 27 | ) 28 | 29 | lazy val root = tlCrossRootProject 30 | .aggregate( 31 | core 32 | ) 33 | 34 | lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) 35 | .in(file("core")) 36 | .settings( 37 | name := "twiddles-core", 38 | libraryDependencies ++= Seq( 39 | "org.typelevel" %%% "cats-core" % "2.13.0", 40 | "org.scalameta" %%% "munit" % "1.1.0" 41 | ) ++ (if (scalaVersion.value.startsWith("2.")) Seq("com.chuusai" %%% "shapeless" % "2.3.13") 42 | else Nil), 43 | scalacOptions := scalacOptions.value.filterNot(_.startsWith("-source:")) 44 | ) 45 | 46 | lazy val coreJVM = core.jvm 47 | .settings( 48 | libraryDependencies ++= 49 | (if (scalaVersion.value.startsWith("2.")) 50 | Seq( 51 | "org.scala-lang" % "scala-reflect" % scalaVersion.value % Test, 52 | "org.scala-lang" % "scala-compiler" % scalaVersion.value % Test 53 | ) 54 | else Nil) 55 | ) 56 | 57 | lazy val coreJS = core.js 58 | .disablePlugins(DoctestPlugin) 59 | .settings( 60 | Test / scalaJSStage := FastOptStage, 61 | jsEnv := new org.scalajs.jsenv.nodejs.NodeJSEnv(), 62 | scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)) 63 | ) 64 | 65 | lazy val coreNative = core.native 66 | .enablePlugins(ScalaNativeBrewedConfigPlugin) 67 | .disablePlugins(DoctestPlugin) 68 | .settings( 69 | tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.9.0").toMap 70 | ) 71 | 72 | lazy val docs = project 73 | .in(file("docs")) 74 | .enablePlugins(MdocPlugin) 75 | .dependsOn(coreJVM) 76 | .settings( 77 | mdocIn := baseDirectory.value / "src", 78 | mdocOut := baseDirectory.value / "..", 79 | githubWorkflowArtifactUpload := false 80 | ) 81 | -------------------------------------------------------------------------------- /core/jvm/src/test/scala-2/org/typelevel/twiddles/test/CompilationPerformanceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles.test 32 | 33 | import munit.FunSuite 34 | import org.typelevel.twiddles.Tuple 35 | 36 | import scala.concurrent._ 37 | import scala.concurrent.duration._ 38 | import scala.reflect.runtime.currentMirror 39 | import scala.reflect.runtime.universe._ 40 | import scala.tools.reflect.ToolBox 41 | 42 | class CompilationPerformanceSpec extends FunSuite { 43 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global 44 | override val munitTimeout: Duration = 30.seconds 45 | 46 | test("should compile long twiddles in a reasonable amount of time") { 47 | val compiled = compileWithin( 48 | q"""import org.typelevel.twiddles._ 49 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 50 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 51 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 52 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 53 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 54 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 55 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 56 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 57 | EmptyTuple""", 58 | 10.seconds 59 | ) 60 | 61 | assert(compiled.isInstanceOf[Tuple]) 62 | } 63 | 64 | test("should subtype in a reasonable amount of time") { 65 | val compiled = compileWithin( 66 | q"""import org.typelevel.twiddles._ 67 | val inferred = 68 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 69 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 70 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 71 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 72 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 73 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 74 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 75 | 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 1 *: 76 | EmptyTuple 77 | type Expected = 78 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 79 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 80 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 81 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 82 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 83 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 84 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 85 | Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: 86 | EmptyTuple 87 | inferred: Expected""", 88 | 10.seconds 89 | ) 90 | 91 | assert(compiled.isInstanceOf[Tuple]) 92 | } 93 | 94 | private val toolbox = { 95 | val toolbox = currentMirror.mkToolBox() 96 | // warmup 97 | toolbox.compile(q"") 98 | toolbox 99 | } 100 | 101 | private def compileWithin(t: Tree, atMost: Duration)(implicit ec: ExecutionContext) = 102 | Await.result(Future(toolbox.compile(t)()), atMost) 103 | } 104 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/DropUnits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import shapeless._ 34 | 35 | /** Describes an isomorphism between two `HList`s, `K` and `L`, where `L` has the same shape as `K` except unit 36 | * values have been removed. 37 | */ 38 | sealed trait DropUnits[K <: HList] { 39 | type L <: HList 40 | def drop(k: K): L 41 | def insert(l: L): K 42 | } 43 | 44 | /** Low priority implicits for [[DropUnits]]. */ 45 | private[twiddles] sealed trait DropUnitsLowPriority { 46 | 47 | /* Keep this low priority so that head of `K` is checked for Unit before this is used. This avoids computing 48 | * H =!:= Unit for each component type. 49 | */ 50 | implicit def `non-empty K and L where head of K and L are same type`[H, KT <: HList, LT <: HList]( 51 | implicit rest: DropUnits.Aux[KT, LT] 52 | ): DropUnits.Aux[H :: KT, H :: LT] = new DropUnits[H :: KT] { 53 | type L = H :: LT 54 | def drop(k: H :: KT): H :: LT = k.head :: rest.drop(k.tail) 55 | def insert(l: H :: LT): H :: KT = l.head :: rest.insert(l.tail) 56 | } 57 | } 58 | 59 | /** Companion for [[DropUnits]]. */ 60 | object DropUnits extends DropUnitsLowPriority { 61 | 62 | type Aux[K0 <: HList, L0 <: HList] = DropUnits[K0] { type L = L0 } 63 | 64 | implicit lazy val base: DropUnits.Aux[HNil, HNil] = new DropUnits[HNil] { 65 | type L = HNil 66 | def drop(k: HNil): HNil = HNil 67 | def insert(l: HNil): HNil = HNil 68 | } 69 | 70 | implicit def `non-empty K and any L where head of K is Unit`[KT <: HList, L0 <: HList](implicit 71 | rest: DropUnits.Aux[KT, L0] 72 | ): DropUnits.Aux[Unit :: KT, L0] = new DropUnits[Unit :: KT] { 73 | type L = L0 74 | def drop(k: Unit :: KT): L = rest.drop(k.tail) 75 | def insert(l: L): Unit :: KT = () :: rest.insert(l) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/Iso.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import shapeless._ 34 | 35 | @annotation.implicitNotFound("""Could not prove ${A} is isomorphic to ${B}.""") 36 | trait Iso[A, B] { 37 | self => 38 | def to(a: A): B 39 | def from(b: B): A 40 | 41 | final def inverse: Iso[B, A] = new Iso[B, A] { 42 | def to(b: B) = self.from(b) 43 | def from(a: A) = self.to(a) 44 | } 45 | } 46 | 47 | private[twiddles] trait IsoLowPriority { 48 | def instance[A, B](t: A => B)(f: B => A): Iso[A, B] = 49 | new Iso[A, B] { 50 | def to(a: A) = t(a) 51 | def from(b: B) = f(b) 52 | } 53 | 54 | implicit def inverse[A, B](implicit iso: Iso[A, B]): Iso[B, A] = iso.inverse 55 | 56 | implicit def productWithUnits[A <: HList, B, Repr <: HList](implicit 57 | g: Generic.Aux[B, Repr], 58 | du: DropUnits.Aux[A, Repr] 59 | ): Iso[A, B] = 60 | instance((a: A) => g.from(du.drop(a)))(b => du.insert(g.to(b))) 61 | } 62 | 63 | /** Companion for [[Iso]]. */ 64 | object Iso extends IsoLowPriority { 65 | 66 | def apply[A, B](implicit instance: Iso[A, B]): Iso[A, B] = instance 67 | 68 | /** Identity iso. */ 69 | implicit def id[A]: Iso[A, A] = instance[A, A](identity)(identity) 70 | 71 | def product[A](implicit gen: Generic[A]): Iso[A, gen.Repr] = 72 | instance[A, gen.Repr](gen.to(_))(gen.from(_)) 73 | 74 | implicit def productInstance[A <: Tuple, B](implicit gen: Generic.Aux[B, A]): Iso[A, B] = 75 | instance[A, B](gen.from)(gen.to) 76 | 77 | implicit def singletonInstance[A, B](implicit gen: Generic.Aux[B, A *: EmptyTuple]): Iso[A, B] = 78 | instance[A, B](a => gen.from(a *: EmptyTuple))(b => gen.to(b).head) 79 | } 80 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/TupleOps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | final class TupleOps[T <: Tuple](private val self: T) extends AnyVal { 34 | def *:[A](a: A): A *: T = new shapeless.::(a, self) 35 | } 36 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/TwiddleCompat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import shapeless.{::, HList, HNil} 34 | import shapeless.ops.hlist.Tupler 35 | 36 | /** Mix-in trait that provides source compatibility between Scala 2 tuples and Twiddles. */ 37 | trait TwiddleCompat { 38 | 39 | type Tuple = HList 40 | @inline val Tuple = HList 41 | 42 | type EmptyTuple = HNil 43 | @inline val EmptyTuple: EmptyTuple = HNil 44 | 45 | type *:[+A, +B <: Tuple] = ::[A, B] 46 | @inline val *: = :: 47 | 48 | implicit def toTupleOps[T <: Tuple](t: T): TupleOps[T] = 49 | new TupleOps[T](t) 50 | 51 | import shapeless.syntax.std.tuple._ 52 | 53 | implicit def tuple2ToHList[A, B](t: (A, B)): A *: B *: EmptyTuple = t.productElements 54 | implicit def tuple3ToHList[A, B, C](t: (A, B, C)): A *: B *: C *: EmptyTuple = t.productElements 55 | implicit def tuple4ToHList[A, B, C, D](t: (A, B, C, D)): A *: B *: C *: D *: EmptyTuple = 56 | t.productElements 57 | implicit def tuple5ToHList[A, B, C, D, E](t: (A, B, C, D, E)): A *: B *: C *: D *: E *: 58 | EmptyTuple = t.productElements 59 | implicit def tuple6ToHList[A, B, C, D, E, F](t: (A, B, C, D, E, F)): A *: B *: C *: D *: E *: F *: 60 | EmptyTuple = t.productElements 61 | implicit def tuple7ToHList[A, B, C, D, E, F, G]( 62 | t: (A, B, C, D, E, F, G) 63 | ): A *: B *: C *: D *: E *: F *: G *: EmptyTuple = t.productElements 64 | implicit def tuple8ToHList[A, B, C, D, E, F, G, H]( 65 | t: (A, B, C, D, E, F, G, H) 66 | ): A *: B *: C *: D *: E *: F *: G *: H *: EmptyTuple = t.productElements 67 | implicit def tuple9ToHList[A, B, C, D, E, F, G, H, I]( 68 | t: (A, B, C, D, E, F, G, H, I) 69 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: EmptyTuple = t.productElements 70 | implicit def tuple10ToHList[A, B, C, D, E, F, G, H, I, J]( 71 | t: (A, B, C, D, E, F, G, H, I, J) 72 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: EmptyTuple = t.productElements 73 | implicit def tuple11ToHList[A, B, C, D, E, F, G, H, I, J, K]( 74 | t: (A, B, C, D, E, F, G, H, I, J, K) 75 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: EmptyTuple = t.productElements 76 | implicit def tuple12ToHList[A, B, C, D, E, F, G, H, I, J, K, L]( 77 | t: (A, B, C, D, E, F, G, H, I, J, K, L) 78 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: EmptyTuple = t.productElements 79 | implicit def tuple13ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M]( 80 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M) 81 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: EmptyTuple = t.productElements 82 | implicit def tuple14ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N]( 83 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) 84 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: EmptyTuple = 85 | t.productElements 86 | implicit def tuple15ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]( 87 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) 88 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: EmptyTuple = 89 | t.productElements 90 | implicit def tuple16ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]( 91 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) 92 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: EmptyTuple = 93 | t.productElements 94 | implicit def tuple17ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q]( 95 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) 96 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: 97 | EmptyTuple = t.productElements 98 | implicit def tuple18ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R]( 99 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) 100 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: R *: 101 | EmptyTuple = t.productElements 102 | implicit def tuple19ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S]( 103 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) 104 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: R *: S *: 105 | EmptyTuple = t.productElements 106 | implicit def tuple20ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T]( 107 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) 108 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: R *: S *: 109 | T *: EmptyTuple = t.productElements 110 | implicit def tuple21ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U]( 111 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) 112 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: R *: S *: 113 | T *: U *: EmptyTuple = t.productElements 114 | implicit def tuple22ToHList[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V]( 115 | t: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) 116 | ): A *: B *: C *: D *: E *: F *: G *: H *: I *: J *: K *: L *: M *: N *: O *: P *: Q *: R *: S *: 117 | T *: U *: V *: EmptyTuple = t.productElements 118 | 119 | // format: off 120 | implicit def hlistToTuple0[Z <: Tuple](z: Z)(implicit 121 | tupler: shapeless.ops.hlist.Tupler.Aux[Z, Unit] 122 | ): Unit = tupler(z) 123 | implicit def hlistToTuple1[Z <: Tuple, A](z: Z)(implicit 124 | tupler: shapeless.ops.hlist.Tupler.Aux[Z, Tuple1[A]] 125 | ): Tuple1[A] = tupler(z) 126 | implicit def hlistToTuple2[Z <: Tuple, A, B](z: Z)(implicit 127 | tupler: Tupler.Aux[Z, (A, B)] 128 | ): (A, B) = tupler(z) 129 | implicit def hlistToTuple3[Z <: Tuple, A, B, C](z: Z)(implicit 130 | tupler: Tupler.Aux[Z, (A, B, C)] 131 | ): (A, B, C) = tupler(z) 132 | implicit def hlistToTuple4[Z <: Tuple, A, B, C, D](z: Z)(implicit 133 | tupler: Tupler.Aux[Z, (A, B, C, D)] 134 | ): (A, B, C, D) = tupler(z) 135 | implicit def hlistToTuple5[Z <: Tuple, A, B, C, D, E](z: Z)(implicit 136 | tupler: Tupler.Aux[Z, (A, B, C, D, E)] 137 | ): (A, B, C, D, E) = tupler(z) 138 | implicit def hlistToTuple6[Z <: Tuple, A, B, C, D, E, F](z: Z)(implicit 139 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F)] 140 | ): (A, B, C, D, E, F) = tupler(z) 141 | implicit def hlistToTuple7[Z <: Tuple, A, B, C, D, E, F, G](z: Z)(implicit 142 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G)] 143 | ): (A, B, C, D, E, F, G) = tupler(z) 144 | implicit def hlistToTuple8[Z <: Tuple, A, B, C, D, E, F, G, H](z: Z)(implicit 145 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H)] 146 | ): (A, B, C, D, E, F, G, H) = tupler(z) 147 | implicit def hlistToTuple9[Z <: Tuple, A, B, C, D, E, F, G, H, I](z: Z)(implicit 148 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I)] 149 | ): (A, B, C, D, E, F, G, H, I) = tupler(z) 150 | implicit def hlistToTuple10[Z <: Tuple, A, B, C, D, E, F, G, H, I, J](z: Z)(implicit 151 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J)] 152 | ): (A, B, C, D, E, F, G, H, I, J) = tupler(z) 153 | implicit def hlistToTuple11[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K](z: Z)(implicit 154 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K)] 155 | ): (A, B, C, D, E, F, G, H, I, J, K) = tupler(z) 156 | implicit def hlistToTuple12[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L](z: Z)(implicit 157 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L)] 158 | ): (A, B, C, D, E, F, G, H, I, J, K, L) = tupler(z) 159 | implicit def hlistToTuple13[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M](z: Z)(implicit 160 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M)] 161 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M) = tupler(z) 162 | implicit def hlistToTuple14[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N](z: Z)(implicit 163 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N)] 164 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N) = tupler(z) 165 | implicit def hlistToTuple15[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O](z: Z)(implicit 166 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O)] 167 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) = tupler(z) 168 | implicit def hlistToTuple16[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P](z: Z)(implicit 169 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P)] 170 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) = tupler(z) 171 | implicit def hlistToTuple17[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q](z: Z)(implicit 172 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q)] 173 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) = tupler(z) 174 | implicit def hlistToTuple18[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R](z: Z)(implicit 175 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R)] 176 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) = tupler(z) 177 | implicit def hlistToTuple19[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S](z: Z)(implicit 178 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S)] 179 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) = tupler(z) 180 | implicit def hlistToTuple20[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T](z: Z)(implicit 181 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T)] 182 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) = tupler(z) 183 | implicit def hlistToTuple21[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U](z: Z)(implicit 184 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U)] 185 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) = tupler(z) 186 | implicit def hlistToTuple22[Z <: Tuple, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V](z: Z)(implicit 187 | tupler: Tupler.Aux[Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V)] 188 | ): (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) = tupler(z) 189 | // format: on 190 | 191 | // Unused but kept for binary compatibility 192 | def hlistToTuple[L <: Tuple, T](l: L)(implicit 193 | tupler: shapeless.ops.hlist.Tupler.Aux[L, T] 194 | ): T = l.tupled 195 | } 196 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/TwiddleOpDropUnits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import cats.Invariant 34 | import cats.syntax.all._ 35 | import shapeless.HList 36 | 37 | final class TwiddleOpDropUnits[F[_], A <: HList](private val self: F[A]) extends AnyVal { 38 | def dropUnits(implicit du: DropUnits[A], F: Invariant[F]): F[du.L] = 39 | self.imap(du.drop(_))(du.insert(_)) 40 | } 41 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/Twiddles.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import cats.{Invariant, InvariantSemigroupal} 34 | import cats.syntax.all._ 35 | import shapeless.HList 36 | 37 | trait TwiddleSyntax[F[_]] { 38 | implicit def toTwiddleOpCons[B <: Tuple](fb: F[B]): TwiddleOpCons[F, B] = new TwiddleOpCons( 39 | fb 40 | ) 41 | implicit def toTwiddleOpTwo[B](fb: F[B]): TwiddleOpTwo[F, B] = new TwiddleOpTwo(fb) 42 | implicit def toTwiddleOpTo[A](fa: F[A]): TwiddleOpTo[F, A] = new TwiddleOpTo(fa) 43 | implicit def toTwiddleOpDropUnits[A <: HList](fa: F[A]): TwiddleOpDropUnits[F, A] = 44 | new TwiddleOpDropUnits(fa) 45 | } 46 | 47 | object syntax { 48 | implicit def toTwiddleOpCons[F[_], B <: Tuple](fb: F[B]): TwiddleOpCons[F, B] = new TwiddleOpCons( 49 | fb 50 | ) 51 | implicit def toTwiddleOpTwo[F[_], B](fb: F[B]): TwiddleOpTwo[F, B] = new TwiddleOpTwo(fb) 52 | implicit def toTwiddleOpTo[F[_], A](fa: F[A]): TwiddleOpTo[F, A] = new TwiddleOpTo(fa) 53 | implicit def toTwiddleOpDropUnits[F[_], A <: HList](fa: F[A]): TwiddleOpDropUnits[F, A] = 54 | new TwiddleOpDropUnits(fa) 55 | } 56 | 57 | final class TwiddleOpCons[F[_], B <: Tuple](private val self: F[B]) extends AnyVal { 58 | def *:[G[x] >: F[x], A](ga: G[A])(implicit G: InvariantSemigroupal[G]): G[A *: B] = 59 | ga.product(self).imap[A *: B] { case (hd, tl) => hd *: tl } { case hd *: tl => (hd, tl) } 60 | } 61 | 62 | final class TwiddleOpTwo[F[_], B](private val self: F[B]) extends AnyVal { 63 | @annotation.nowarn 64 | def *:[G[x] >: F[x], A](ga: G[A])(implicit G: InvariantSemigroupal[G]): G[A *: B *: EmptyTuple] = 65 | ga.product(self).imap[A *: B *: EmptyTuple] { case (a, b) => a *: b *: EmptyTuple } { 66 | case a *: b *: EmptyTuple => (a, b) 67 | } 68 | } 69 | final class TwiddleOpTo[F[_], A](private val self: F[A]) extends AnyVal { 70 | def to[B](implicit iso: Iso[A, B], F: Invariant[F]): F[B] = self.imap(iso.to)(iso.from) 71 | } 72 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-2/org/typelevel/twiddles/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel 32 | 33 | package object twiddles extends TwiddleCompat 34 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-3/org/typelevel/twiddles/DropUnits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import scala.compiletime.* 34 | 35 | /** The tuple which is the result of removing all 'Unit' types from the tuple 'A'. */ 36 | type DropUnits[A <: Tuple] <: Tuple = A match 37 | case hd *: tl => 38 | hd match 39 | case Unit => DropUnits[tl] 40 | case Any => hd *: DropUnits[tl] 41 | case EmptyTuple => EmptyTuple 42 | 43 | object DropUnits: 44 | 45 | inline def drop[A <: Tuple](a: A): DropUnits[A] = 46 | // Ideally, the following would work: 47 | // inline a match 48 | // case (_ *: t): (Unit *: tl) => drop[tl](t) 49 | // case (h *: t): (hd *: tl) => h *: drop[tl](t) 50 | // case EmptyTuple => EmptyTuple 51 | (inline erasedValue[A] match 52 | case _: (Unit *: tl) => drop[tl](a.asInstanceOf[Unit *: tl].tail) 53 | case _: (hd *: tl) => 54 | val at = a.asInstanceOf[hd *: tl] 55 | at.head *: drop[tl](at.tail) 56 | case EmptyTuple => EmptyTuple 57 | ).asInstanceOf[DropUnits[A]] 58 | 59 | inline def insert[A <: Tuple](t: DropUnits[A]): A = 60 | (inline erasedValue[A] match 61 | case _: (Unit *: tl) => (()) *: (insert[tl](t.asInstanceOf[DropUnits[tl]])) 62 | case _: (hd *: tl) => 63 | val t2 = t.asInstanceOf[NonEmptyTuple] 64 | t2.head.asInstanceOf[hd] *: insert[tl](t2.tail.asInstanceOf[DropUnits[tl]]) 65 | case EmptyTuple => EmptyTuple 66 | ).asInstanceOf[A] 67 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-3/org/typelevel/twiddles/Iso.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import scala.deriving.Mirror 34 | 35 | @annotation.implicitNotFound("""Could not prove ${A} is isomorphic to ${B}.""") 36 | trait Iso[A, B] { 37 | self => 38 | def to(a: A): B 39 | def from(b: B): A 40 | 41 | final def inverse: Iso[B, A] = new Iso[B, A] { 42 | def to(b: B) = self.from(b) 43 | def from(a: A) = self.to(a) 44 | } 45 | } 46 | 47 | private trait IsoLowPriority { 48 | def instance[A, B](t: A => B)(f: B => A): Iso[A, B] = 49 | new Iso[A, B] { 50 | def to(a: A) = t(a) 51 | def from(b: B) = f(b) 52 | } 53 | 54 | given inverse[A, B](using iso: Iso[A, B]): Iso[B, A] = iso.inverse 55 | 56 | inline given productWithUnits[A <: Tuple, B <: Product](using 57 | m: Mirror.ProductOf[B] { type MirroredElemTypes = DropUnits[A] } 58 | ): Iso[A, B] = 59 | instance((a: A) => m.fromProduct(DropUnits.drop(a)))(b => 60 | DropUnits.insert(Tuple.fromProductTyped(b)) 61 | ) 62 | } 63 | 64 | /** Companion for [[Iso]]. */ 65 | object Iso extends IsoLowPriority { 66 | 67 | def apply[A, B](implicit instance: Iso[A, B]): Iso[A, B] = instance 68 | 69 | /** Identity iso. */ 70 | given id[A]: Iso[A, A] = instance[A, A](identity)(identity) 71 | 72 | def product[A <: Product](using m: Mirror.ProductOf[A]): Iso[A, m.MirroredElemTypes] = 73 | instance[A, m.MirroredElemTypes](Tuple.fromProductTyped)(m.fromProduct) 74 | 75 | given productInstance[A <: Tuple, B <: Product](using 76 | m: Mirror.ProductOf[B] { type MirroredElemTypes = A } 77 | ): Iso[A, B] = 78 | instance[A, B](m.fromProduct)(Tuple.fromProductTyped) 79 | 80 | given singletonInstance[A, B <: Product](using 81 | m: Mirror.ProductOf[B] { type MirroredElemTypes = A *: EmptyTuple } 82 | ): Iso[A, B] = 83 | instance[A, B](a => m.fromProduct(a *: EmptyTuple))(b => Tuple.fromProductTyped(b).head) 84 | } 85 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-3/org/typelevel/twiddles/TwiddleCompat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | trait TwiddleCompat 34 | -------------------------------------------------------------------------------- /core/shared/src/main/scala-3/org/typelevel/twiddles/Twiddles.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package org.typelevel.twiddles 32 | 33 | import cats.{Invariant, InvariantSemigroupal} 34 | import cats.syntax.all._ 35 | 36 | trait TwiddleSyntax[F[_]]: 37 | 38 | implicit def toTwiddleOpCons[B <: Tuple](fb: F[B]): TwiddleOpCons[F, B] = new TwiddleOpCons( 39 | fb 40 | ) 41 | implicit def toTwiddleOpTwo[B](fb: F[B]): TwiddleOpTwo[F, B] = new TwiddleOpTwo(fb) 42 | 43 | implicit def toTwiddleOpTo[A](fa: F[A]): TwiddleOpTo[F, A] = new TwiddleOpTo(fa) 44 | 45 | extension [A <: Tuple](fa: F[A]) 46 | inline def dropUnits(using Invariant[F]): F[DropUnits[A]] = 47 | fa.imap(DropUnits.drop(_))(DropUnits.insert(_)) 48 | 49 | object syntax: 50 | extension [F[_], A](fa: F[A]) 51 | @annotation.targetName("cons") 52 | def *:[G[x] >: F[x], B <: Tuple](gb: G[B])(using InvariantSemigroupal[G]): G[A *: B] = 53 | fa.product(gb).imap[A *: B] { case (hd, tl) => hd *: tl } { case hd *: tl => (hd, tl) } 54 | 55 | @annotation.targetName("pair") 56 | @annotation.nowarn 57 | def *:[G[x] >: F[x], B](gb: G[B])(using InvariantSemigroupal[G]): G[A *: B *: EmptyTuple] = 58 | fa.product(gb).imap[A *: B *: EmptyTuple] { case (a, b) => a *: b *: EmptyTuple } { 59 | case a *: b *: EmptyTuple => (a, b) 60 | } 61 | 62 | def to[B](using iso: Iso[A, B], F: Invariant[F]): F[B] = fa.imap(iso.to)(iso.from) 63 | 64 | extension [F[_], A <: Tuple](fa: F[A]) 65 | inline def dropUnits(using Invariant[F]): F[DropUnits[A]] = 66 | fa.imap(DropUnits.drop(_))(DropUnits.insert(_)) 67 | 68 | final class TwiddleOpCons[F[_], B <: Tuple](private val self: F[B]) extends AnyVal: 69 | // Workaround for https://github.com/typelevel/twiddles/pull/2 70 | @annotation.targetName("consFixedF") 71 | def *:[A](fa: F[A])(using F: InvariantSemigroupal[F]): F[A *: B] = 72 | *:[F, A](fa)(using F) 73 | 74 | def *:[G[x] >: F[x], A](ga: G[A])(using InvariantSemigroupal[G]): G[A *: B] = 75 | ga.product(self).imap[A *: B] { case (hd, tl) => hd *: tl } { case hd *: tl => (hd, tl) } 76 | 77 | final class TwiddleOpTwo[F[_], B](private val self: F[B]) extends AnyVal: 78 | // Workaround for https://github.com/typelevel/twiddles/pull/2 79 | @annotation.targetName("twoFixedF") 80 | def *:[A]( 81 | fa: F[A] 82 | )(using F: InvariantSemigroupal[F]): F[A *: B *: EmptyTuple] = 83 | *:[F, A](fa)(using F) 84 | 85 | def *:[G[x] >: F[x], A](ga: G[A])(using InvariantSemigroupal[G]): G[A *: B *: EmptyTuple] = 86 | ga.product(self).imap[A *: B *: EmptyTuple] { case (a, b) => a *: b *: EmptyTuple } { 87 | case a *: b *: EmptyTuple => (a, b) 88 | } 89 | 90 | final class TwiddleOpTo[F[_], A](private val self: F[A]) extends AnyVal: 91 | def to[B](implicit iso: Iso[A, B], F: Invariant[F]): F[B] = self.imap(iso.to)(iso.from) 92 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/examples/Example.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package examples 32 | 33 | import org.typelevel.twiddles._ 34 | import cats.Applicative 35 | 36 | object Example { 37 | 38 | trait Json 39 | trait Decoder[A] { 40 | def decode(s: Json): Option[A] 41 | } 42 | object Decoder extends TwiddleSyntax[Decoder] { 43 | implicit val applicative: Applicative[Decoder] = new Applicative[Decoder] { 44 | def pure[A](a: A): Decoder[A] = _ => Some(a) 45 | def ap[A, B](ff: Decoder[A => B])(fa: Decoder[A]): Decoder[B] = j => 46 | for { 47 | a <- fa.decode(j) 48 | f <- ff.decode(j) 49 | } yield f(a) 50 | } 51 | } 52 | 53 | val int: Decoder[Int] = _ => ??? 54 | val string: Decoder[String] = _ => ??? 55 | 56 | case class Foo(x: Int, y: String) 57 | val fooDecoder: Decoder[Foo] = (int *: string).to[Foo] 58 | 59 | case class Bar(x: Int) 60 | val barDecoder: Decoder[Bar] = int.to[Bar] 61 | 62 | case class Baz(x: Foo, y: Bar) 63 | val bazDecoder = (fooDecoder *: barDecoder).to[Baz] 64 | 65 | val unit: Decoder[Unit] = _ => ??? 66 | val fooDecoder2 = (int *: unit *: string *: unit).to[Foo] 67 | 68 | val dropping: Decoder[Int *: String *: EmptyTuple] = 69 | (int *: unit *: string *: unit).dropUnits 70 | 71 | def foo(t: Int *: String *: Boolean *: EmptyTuple): Unit = ??? 72 | foo((1, "hi", true)) 73 | 74 | def bar(t: (Int, String, Boolean)): Unit = ??? 75 | bar(1 *: "hi" *: true *: EmptyTuple) 76 | 77 | val bazIso = Iso.product[Baz] 78 | 79 | { 80 | // Make sure there's no conflict with cats syntax 81 | import cats.syntax.all._ 82 | val d: Decoder[Foo] = (int *: string).to[Foo] 83 | val _: Decoder[Int] = d.as(42) // as from Functor 84 | } 85 | 86 | { 87 | import org.typelevel.twiddles.syntax._ 88 | val _ = Option(1) *: Option(2) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/examples/Hierarchy.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Typelevel 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors 16 | * may be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package examples 32 | 33 | import cats.InvariantSemigroupal 34 | import cats.ContravariantSemigroupal 35 | import cats.Applicative 36 | import org.typelevel.twiddles.TwiddleSyntax 37 | 38 | object Hierarchy { 39 | trait Encoder[A] 40 | object Encoder extends TwiddleSyntax[Encoder] { 41 | implicit def instance: ContravariantSemigroupal[Encoder] = ??? 42 | } 43 | 44 | trait Decoder[A] 45 | object Decoder extends TwiddleSyntax[Decoder] { 46 | implicit def instance: Applicative[Decoder] = ??? 47 | } 48 | 49 | trait Codec[A] extends Encoder[A] with Decoder[A] 50 | object Codec extends TwiddleSyntax[Codec] { 51 | implicit def instance: InvariantSemigroupal[Codec] = ??? 52 | } 53 | 54 | def int: Codec[Int] = ??? 55 | def string: Codec[String] = ??? 56 | def bool: Codec[Boolean] = ??? 57 | 58 | case class Foo(x: Int, y: String, z: Boolean) 59 | 60 | def a: Codec[Foo] = (int *: string *: bool).to[Foo] 61 | def b: Decoder[Foo] = (int *: string *: (bool: Decoder[Boolean])).to[Foo] 62 | def c: Decoder[Foo] = (int *: (string: Decoder[String]) *: bool).to[Foo] 63 | def d: Decoder[Foo] = ((int: Decoder[Int]) *: string *: bool).to[Foo] 64 | def e: Encoder[Foo] = (int *: string *: (bool: Encoder[Boolean])).to[Foo] 65 | def f: Encoder[Foo] = (int *: (string: Encoder[String]) *: bool).to[Foo] 66 | def g: Encoder[Foo] = ((int: Encoder[Int]) *: string *: bool).to[Foo] 67 | def h = (int *: string) *: bool 68 | def i = (int *: string *: bool) *: bool *: bool 69 | def j: Codec[Foo] = (int *: string *: bool).to 70 | } 71 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # Twiddles 2 | 3 | A twiddle list is a list of one or more values, potentially of differing types, that supports incremental creation and supports conversion to case classes that are "shape compatible" with the constituent types of the twiddle list. 4 | 5 | Twiddle lists are useful in the creation of protocols (e.g., decoders, encoders, codecs), where a protocol for a complex type is built from simpler constituent protocols. This technique was first popularized by parser combinators with syntax like `lparen ~ expr ~ rparen`. In contrast to type driven derivation schemes, where protocols are implicitly determined by the constituent types of a data constructor, twiddle lists keep the focus on the protocol. 6 | 7 | This library provides the ability to work with twiddle lists for arbitrary types and provides a single API that works for both Scala 3 and Scala 2. On Scala 3, twiddle lists are represented as generic tuples -- e.g., `F[Int *: String *: Boolean *: EmptyTuple]` or equivalently `F[(Int, String, Boolean)]`. On Scala 2, twiddle lists are represented as Shapeless heterogeneous lists. The `org.typelevel.twiddles` package provides type aliases that allow for source compatibility (`*:` is aliased to `shapeles.::` and `EmptyTuple` is aliased to `shapeless.HNil`). 8 | 9 | ## Getting Started 10 | 11 | Artifacts are published for Scala 2.12, 2.13, and 3 and all platforms (JVM, Scala.js, and Scala Native). 12 | 13 | ```scala 14 | libraryDependencies += "org.typelevel" %%% "twiddles-core" % "0.7.2" // check Releases for the latest version 15 | ``` 16 | 17 | ```scala mdoc 18 | // Enable twiddle syntax for arbitrary types 19 | import org.typelevel.twiddles.syntax._ 20 | 21 | case class Foo(x: Int, y: String) 22 | 23 | val a = Option(42) 24 | val b = Option("Hi") 25 | 26 | val foo = (a *: b).to[Foo] 27 | ``` 28 | 29 | In this example, `a *: b` creates an `Option[Int *: String *: EmptyTuple]`. We then convert that value to an `Option[Foo]` via `.to[Foo]`. 30 | 31 | The `*:` operation comes from the imported twiddle syntax and is similar to the Scala 3 built-in tuple cons operation, but works on applied type constructors. The expression `a *: b *: c` builds an `F[A *: B *: C *: EmptyTuple]` from an `F[A]`, `F[B]`, and `F[C]`. The `*:` operation requires that the type constructor `F` has a `cats.InvariantSemigroupal` instance. 32 | 33 | The `to` operation also comes from the imported twiddle syntax. Calling `.to[X]` on an `F[T]` for some twiddle list `T` results in an `F[X]` provided that `T` is shape compatible with `X`. In the most common case where `X` is a case class, shape compatibility is defined as `T` having the same types in the same order as the parameters of `X`. The `to` operation requires that the type constructor `F` has a `cats.Invariant` instance. 34 | 35 | Invariant semigroupals are much more general than (covariant) functors, which means twiddle list support works for a wide variety of data types. For instance, contravariant functors are invariant semigroupals allowing us to use twiddle list syntax to incrementally build instances: 36 | 37 | ```scala mdoc 38 | val fooOrdering = (summon[Ordering[Int]] *: summon[Ordering[String]]).to[Foo] 39 | ``` 40 | 41 | ## Library Usage 42 | 43 | When designing a library that uses twiddle lists, the `TwiddleSyntax` trait can be mixed in to the companion object of a type constructor. This has the effect of providing twiddle syntax without requiring users of the library to import `org.typelevel.twiddles.syntax._` at each call site. 44 | 45 | ```scala mdoc:reset 46 | import org.typelevel.twiddles.TwiddleSyntax 47 | import cats.Applicative 48 | 49 | trait Json 50 | trait Decoder[A] { 51 | def decode(j: Json): Option[A] 52 | } 53 | object Decoder extends TwiddleSyntax[Decoder] { 54 | implicit val applicative: Applicative[Decoder] = new Applicative[Decoder] { 55 | def pure[A](a: A): Decoder[A] = _ => Some(a) 56 | def ap[A, B](ff: Decoder[A => B])(fa: Decoder[A]): Decoder[B] = j => 57 | for { 58 | f <- ff.decode(j) 59 | a <- fa.decode(j) 60 | } yield f(a) 61 | } 62 | } 63 | 64 | val int: Decoder[Int] = _ => ??? 65 | val string: Decoder[String] = _ => ??? 66 | 67 | case class Foo(x: Int, y: String) 68 | val fooDecoder = (int *: string).to[Foo] 69 | ``` 70 | 71 | In this example, the `Decoder` type has an `Applicative` instance defined in its companion object (and `Applicative` extends `InvariantSemigroupal`), and the companion object extends `TwiddleSyntax`. The latter enables use of `*:` and `to` with `Decoder` values without adding explicit imports (that is, there's no need to import `org.typelevel.twiddles.syntax._` at call sites). 72 | 73 | ## Etymology 74 | 75 | The term "twiddle list" was first coined by [Rob Norris](https://github.com/tpolecat) in the [Skunk](https://github.com/tpolecat/skunk) library, where a twiddle list was defined as a left nested tuple. For example, a 4 element twiddle list consisting of an `Int`, `String`, `Boolean`, and `Double` was represented as `(((Int, String), Boolean), Double)`. 76 | 77 | This library uses a different encoding -- twiddle lists are encoded as tuples on Scala 3 and Shapeless heterogeneous lists on Scala 2. The previous 4 element twiddle list is represented as `Int *: String *: Boolean *: Double *: EmptyTuple`. 78 | 79 | We adopt the name "twiddle list" to refer to the general technique of incremental construction of complex protocols. 80 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val sbtTypelevelVersion = "0.8.0" 2 | addSbtPlugin("org.typelevel" % "sbt-typelevel" % sbtTypelevelVersion) 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") 4 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 5 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 6 | addSbtPlugin("com.armanbilge" % "sbt-scala-native-config-brew-github-actions" % "0.4.0") 7 | addSbtPlugin("io.github.sbt-doctest" % "sbt-doctest" % "0.11.1") 8 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.5") 9 | 10 | libraryDependencySchemes += "com.lihaoyi" %% "geny" % VersionScheme.Always 11 | --------------------------------------------------------------------------------