├── .github ├── pr-labeler.yml ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── release-drafter.yml │ └── release.yml ├── .gitignore ├── .ruby-version ├── .scalafmt.conf ├── AUTHORS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── build.sbt ├── docs ├── AUTHORS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md └── NOTICE.md ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── cats.svg └── scala │ └── catslib │ ├── Applicative.scala │ ├── Apply.scala │ ├── ApplyHelpers.scala │ ├── CatsLibrary.scala │ ├── EitherSection.scala │ ├── EvalSection.scala │ ├── Foldable.scala │ ├── FunctorSection.scala │ ├── IdentitySection.scala │ ├── Monad.scala │ ├── MonadHelpers.scala │ ├── Monoid.scala │ ├── OptionTSection.scala │ ├── Semigroup.scala │ ├── Traverse.scala │ ├── TraverseHelpers.scala │ ├── Validated.scala │ └── ValidatedHelpers.scala └── test └── scala └── catslib ├── ApplicativeSpec.scala ├── ApplySpec.scala ├── EitherSpec.scala ├── EvalSpec.scala ├── FoldableSpec.scala ├── FunctorSpec.scala ├── IdentitySpec.scala ├── MonadSpec.scala ├── MonoidSpec.scala ├── OptionTSpec.scala ├── SemigroupSpec.scala ├── TraverseSpec.scala └── ValidatedSpec.scala /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | enhancement: ['enhancement/*', 'feature/*'] 6 | documentation: ['docs/*', 'doc/*'] 7 | breaking-change: ['breaking/*', 'break/*'] 8 | bug: ['bug/*', 'fix/*'] 9 | tests: ['test/*', 'tests/*'] 10 | dependency-update: ['dep/*', 'dependency/*', 'dependency-update/*'] 11 | scala-steward: ['update/*'] 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | name-template: 'v$NEXT_PATCH_VERSION' 6 | tag-template: 'v$NEXT_PATCH_VERSION' 7 | exclude-labels: 8 | - 'auto-update' 9 | - 'auto-documentation' 10 | - 'auto-changelog' 11 | categories: 12 | - title: '⚠️ Breaking changes' 13 | label: 'breaking-change' 14 | - title: '🚀 Features' 15 | label: 'enhancement' 16 | - title: '📘 Documentation' 17 | label: 'documentation' 18 | - title: '🐛 Bug Fixes' 19 | label: 'bug' 20 | - title: '📈 Dependency updates' 21 | labels: 22 | - 'dependency-update' 23 | - 'scala-steward' 24 | template: | 25 | ## What's changed 26 | 27 | $CHANGES 28 | 29 | ## Contributors to this release 30 | 31 | $CONTRIBUTORS 32 | 33 | autolabeler: 34 | - label: "enhancement" 35 | branch: 36 | - '/enhancement\/.+/' 37 | - '/feature\/.+/' 38 | - label: "documentation" 39 | files: 40 | - "*.md" 41 | branch: 42 | - '/docs\/.+/' 43 | - '/doc\/.+/' 44 | - label: "breaking-change" 45 | branch: 46 | - '/breaking\/.+/' 47 | - '/break\/.+/' 48 | - label: "bug" 49 | branch: 50 | - '/bug\/.+/' 51 | - '/fix\/.+/' 52 | - label: "tests" 53 | branch: 54 | - '/test\/.+/' 55 | - '/tests\/.+/' 56 | - label: "dependency-update" 57 | branch: 58 | - '/dep\/.+/' 59 | - '/dependency\/.+/' 60 | - '/dependency-update\/.+/' 61 | - label: "scala-steward" 62 | branch: 63 | - '/update\/.+/' 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | name: Formatters & Tests 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | 12 | jobs: 13 | test: 14 | if: "!contains(github.event.head_commit.message, 'skip ci')" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout project (pull-request) 18 | if: github.event_name == 'pull_request' 19 | uses: actions/checkout@v2.3.2 20 | with: 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Checkout project (main) 24 | if: github.event_name == 'push' 25 | uses: actions/checkout@v2 26 | - name: Setup Scala 27 | uses: olafurpg/setup-scala@v11 28 | with: 29 | java-version: adopt@1.11 30 | - name: Setup Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: .ruby-version 34 | - name: Setup yq 35 | run: sudo snap install yq 36 | - name: Run pre-conditions 37 | run: test -f .github/actions.yml && eval "$(yq e '.pre.ci // "true"' .github/actions.yml)" || true 38 | - name: Run scalafmt on Scala Steward PRs 39 | if: github.event.pull_request.user.login == '47erbot' && contains(github.event.pull_request.body, 'Scala Steward') 40 | run: sbt "scalafixEnable; fix" || sbt "scalafmtAll; scalafmtSbt" || true 41 | - name: Push changes 42 | uses: stefanzweifel/git-auto-commit-action@v4.5.1 43 | with: 44 | commit_message: Run formatter/linter 45 | - name: Run checks 46 | run: sbt ci-test 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 49 | - name: Run post-conditions 50 | run: test -f .github/actions.yml && eval "$(yq e '.post.ci // "true"' .github/actions.yml)" || true 51 | - name: Automerge Scala Steward PRs 52 | if: success() && github.event_name == 'pull_request' && contains(github.event.pull_request.body, 'Scala Steward') 53 | uses: ridedott/merge-me-action@v1.1.36 54 | with: 55 | GITHUB_LOGIN: 47erbot 56 | GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | name: Update documentation 6 | 7 | on: 8 | release: 9 | types: [published] 10 | repository_dispatch: 11 | types: [docs] 12 | 13 | jobs: 14 | documentation: 15 | if: "!contains(github.event.head_commit.message, 'skip ci')" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout project 19 | uses: actions/checkout@v2 20 | with: 21 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 22 | ref: main 23 | - name: Fetch tags 24 | run: git fetch --tags 25 | - name: Setup Scala 26 | uses: olafurpg/setup-scala@v11 27 | with: 28 | java-version: adopt@1.11 29 | - name: Setup Ruby 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: .ruby-version 33 | - name: Setup github-changelog-generator 34 | run: gem install github_changelog_generator -v 1.15.0 35 | - name: Setup yq 36 | run: sudo snap install yq 37 | - name: Run pre-conditions 38 | run: test -f .github/actions.yml && eval "$(yq e '.pre.docs // "true"' .github/actions.yml)" || true 39 | - name: Generate documentation 40 | run: sbt ci-docs 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | DOWNLOAD_INFO_FROM_GITHUB: true 44 | - name: Run post-conditions 45 | run: test -f .github/actions.yml && eval "$(yq e '.post.docs // "true"' .github/actions.yml)" || true 46 | - name: Push changes 47 | uses: stefanzweifel/git-auto-commit-action@v4.1.3 48 | with: 49 | commit_message: 'Update documentation, and other files [skip ci]' 50 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Drafts/updates the next repository release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | name: Release 6 | 7 | on: 8 | release: 9 | types: [published] 10 | push: 11 | branches: main 12 | 13 | jobs: 14 | release: 15 | if: "!contains(github.event.head_commit.message, 'skip ci')" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout project 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - name: Fetch tags 23 | run: git fetch --tags 24 | - name: Setup Scala 25 | uses: olafurpg/setup-scala@v11 26 | with: 27 | java-version: adopt@1.11 28 | - name: Setup Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: .ruby-version 32 | - name: Setup GPG 33 | uses: olafurpg/setup-gpg@v3 34 | - name: Setup yq 35 | run: sudo snap install yq 36 | - name: Run pre-conditions 37 | run: test -f .github/actions.yml && eval "$(yq e '.pre.release // "true"' .github/actions.yml)" || true 38 | - name: Release new version 39 | run: sbt ci-publish 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 43 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 44 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 45 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 46 | - name: Run post-conditions 47 | run: test -f .github/actions.yml && eval "$(yq e '.post.release // "true"' .github/actions.yml)" || true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't edit this file! 2 | # It is automatically updated after every release of https://github.com/47degrees/.github 3 | # If you want to suggest a change, please open a PR or issue in that repository 4 | 5 | ### Intellij ### 6 | 7 | .idea 8 | out/ 9 | 10 | ### Java ### 11 | 12 | *.class 13 | *.log 14 | 15 | ### macOS ### 16 | 17 | .DS_Store 18 | 19 | ### SBT ### 20 | 21 | dist/* 22 | target/ 23 | lib_managed/ 24 | src_managed/ 25 | project/boot/ 26 | project/plugins/project/ 27 | .history 28 | .cache 29 | .lib/ 30 | .bsp 31 | 32 | ### Scala ### 33 | 34 | *.metals 35 | .bloop/ 36 | .metals/ 37 | metals.sbt 38 | .scala-build/ 39 | 40 | ### Vim ### 41 | 42 | # Swap 43 | [._]*.s[a-v][a-z] 44 | [._]*.sw[a-p] 45 | [._]s[a-rt-v][a-z] 46 | [._]ss[a-gi-z] 47 | [._]sw[a-p] 48 | 49 | # Session 50 | Session.vim 51 | Sessionx.vim 52 | 53 | # Temporary 54 | .netrwhist 55 | 56 | # Project local build artefacts 57 | .output 58 | 59 | # Auto-generated tag files 60 | tags 61 | 62 | # Persistent undo 63 | [._]*.un~ 64 | 65 | # Coc configuration directory 66 | .vim 67 | 68 | ### VisualStudioCode ### 69 | 70 | .vscode/ 71 | .vscode/* 72 | !.vscode/settings.json 73 | !.vscode/tasks.json 74 | !.vscode/launch.json 75 | !.vscode/extensions.json 76 | 77 | # Direnv 78 | 79 | .direnv 80 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.2 -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=3.7.3 2 | style = defaultWithAlign 3 | maxColumn = 100 4 | runner.dialect = scala213 5 | 6 | continuationIndent.callSite = 2 7 | 8 | newlines { 9 | sometimesBeforeColonInMethodReturnType = false 10 | } 11 | 12 | align { 13 | arrowEnumeratorGenerator = false 14 | ifWhileOpenParen = false 15 | openParenCallSite = false 16 | openParenDefnSite = false 17 | } 18 | 19 | docstrings.style = Asterisk 20 | 21 | rewrite { 22 | rules = [SortImports, RedundantBraces] 23 | redundantBraces.maxLines = 1 24 | } 25 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Authors 6 | 7 | ## Maintainers 8 | 9 | The maintainers of the project are: 10 | 11 | - [![47erbot](https://avatars1.githubusercontent.com/u/24799081?v=4&s=20) **47erbot**](https://github.com/47erbot) 12 | - [![alejandrohdezma](https://avatars0.githubusercontent.com/u/9027541?v=4&s=20) **Alejandro Hernández (alejandrohdezma)**](https://github.com/alejandrohdezma) 13 | - [![angoglez](https://avatars0.githubusercontent.com/u/10107285?v=4&s=20) **Ana Gómez González (angoglez)**](https://github.com/angoglez) 14 | - [![andyscott](https://avatars3.githubusercontent.com/u/310363?v=4&s=20) **Andy Scott (andyscott)**](https://github.com/andyscott) 15 | - [![BenFradet](https://avatars2.githubusercontent.com/u/1737211?v=4&s=20) **Ben Fradet (BenFradet)**](https://github.com/BenFradet) 16 | - [![dominv](https://avatars1.githubusercontent.com/u/3943031?v=4&s=20) **Domingo Valera (dominv)**](https://github.com/dominv) 17 | - [![kiroco12](https://avatars1.githubusercontent.com/u/48894338?v=4&s=20) **Enrique Nieto (kiroco12)**](https://github.com/kiroco12) 18 | - [![FRosner](https://avatars2.githubusercontent.com/u/3427394?v=4&s=20) **Frank Rosner (FRosner)**](https://github.com/FRosner) 19 | - [![jdesiloniz](https://avatars2.githubusercontent.com/u/2835739?v=4&s=20) **Javier de Silóniz Sandino (jdesiloniz)**](https://github.com/jdesiloniz) 20 | - [![juanpedromoreno](https://avatars2.githubusercontent.com/u/4879373?v=4&s=20) **Juan Pedro Moreno (juanpedromoreno)**](https://github.com/juanpedromoreno) 21 | - [![rafaparadela](https://avatars3.githubusercontent.com/u/315070?v=4&s=20) **Rafa Paradela (rafaparadela)**](https://github.com/rafaparadela) 22 | - [![raulraja](https://avatars3.githubusercontent.com/u/456796?v=4&s=20) **Raúl Raja Martínez (raulraja)**](https://github.com/raulraja) 23 | 24 | ## Contributors 25 | 26 | These are the people that have contributed to the _exercises-cats_ project: 27 | 28 | - [![juanpedromoreno](https://avatars2.githubusercontent.com/u/4879373?v=4&s=20) **juanpedromoreno**](https://github.com/juanpedromoreno) 29 | - [![47erbot](https://avatars1.githubusercontent.com/u/24799081?v=4&s=20) **47erbot**](https://github.com/47erbot) 30 | - [![raulraja](https://avatars3.githubusercontent.com/u/456796?v=4&s=20) **raulraja**](https://github.com/raulraja) 31 | - [![kiroco12](https://avatars1.githubusercontent.com/u/48894338?v=4&s=20) **kiroco12**](https://github.com/kiroco12) 32 | - [![rafaparadela](https://avatars3.githubusercontent.com/u/315070?v=4&s=20) **rafaparadela**](https://github.com/rafaparadela) 33 | - [![aeons](https://avatars2.githubusercontent.com/u/1432894?v=4&s=20) **aeons**](https://github.com/aeons) 34 | - [![dominv](https://avatars1.githubusercontent.com/u/3943031?v=4&s=20) **dominv**](https://github.com/dominv) 35 | - [![jdesiloniz](https://avatars2.githubusercontent.com/u/2835739?v=4&s=20) **jdesiloniz**](https://github.com/jdesiloniz) 36 | - [![BenFradet](https://avatars2.githubusercontent.com/u/1737211?v=4&s=20) **BenFradet**](https://github.com/BenFradet) 37 | - [![doub1ejack](https://avatars2.githubusercontent.com/u/1809882?v=4&s=20) **doub1ejack**](https://github.com/doub1ejack) 38 | - [![yilinwei](https://avatars0.githubusercontent.com/u/8933128?v=4&s=20) **yilinwei**](https://github.com/yilinwei) 39 | - [![angoglez](https://avatars0.githubusercontent.com/u/10107285?v=4&s=20) **angoglez**](https://github.com/angoglez) 40 | - [![andyscott](https://avatars3.githubusercontent.com/u/310363?v=4&s=20) **andyscott**](https://github.com/andyscott) 41 | - [![ches](https://avatars3.githubusercontent.com/u/13277?v=4&s=20) **ches**](https://github.com/ches) 42 | - [![dmarticus](https://avatars2.githubusercontent.com/u/4853149?v=4&s=20) **dmarticus**](https://github.com/dmarticus) 43 | - [![FRosner](https://avatars2.githubusercontent.com/u/3427394?v=4&s=20) **FRosner**](https://github.com/FRosner) 44 | - [![ikhoon](https://avatars1.githubusercontent.com/u/1866157?v=4&s=20) **ikhoon**](https://github.com/ikhoon) 45 | - [![jkmcclellan](https://avatars3.githubusercontent.com/u/52432856?v=4&s=20) **jkmcclellan**](https://github.com/jkmcclellan) 46 | - [![jeremylaier](https://avatars1.githubusercontent.com/u/50848217?v=4&s=20) **jeremylaier**](https://github.com/jeremylaier) 47 | - [![chipmunklimo](https://avatars3.githubusercontent.com/u/4879096?v=4&s=20) **chipmunklimo**](https://github.com/chipmunklimo) 48 | - [![mayonesa](https://avatars3.githubusercontent.com/u/56529?v=4&s=20) **mayonesa**](https://github.com/mayonesa) 49 | - [![lloydmeta](https://avatars3.githubusercontent.com/u/914805?v=4&s=20) **lloydmeta**](https://github.com/lloydmeta) 50 | - [![mttkay](https://avatars3.githubusercontent.com/u/102802?v=4&s=20) **mttkay**](https://github.com/mttkay) 51 | - [![paualarco](https://avatars0.githubusercontent.com/u/33580722?v=4&s=20) **paualarco**](https://github.com/paualarco) 52 | - [![richardmiller](https://avatars1.githubusercontent.com/u/783827?v=4&s=20) **richardmiller**](https://github.com/richardmiller) 53 | - [![Samehadar](https://avatars2.githubusercontent.com/u/15636676?v=4&s=20) **Samehadar**](https://github.com/Samehadar) 54 | - [![sergiohgz](https://avatars3.githubusercontent.com/u/14012309?v=4&s=20) **sergiohgz**](https://github.com/sergiohgz) 55 | - [![Thangiee](https://avatars2.githubusercontent.com/u/4734933?v=4&s=20) **Thangiee**](https://github.com/Thangiee) 56 | - [![TobiasRoland](https://avatars2.githubusercontent.com/u/5878818?v=4&s=20) **TobiasRoland**](https://github.com/TobiasRoland) 57 | - [![alejandrohdezma](https://avatars0.githubusercontent.com/u/9027541?v=4&s=20) **alejandrohdezma**](https://github.com/alejandrohdezma) 58 | - [![tr4rex](https://avatars1.githubusercontent.com/u/13753704?v=4&s=20) **tr4rex**](https://github.com/tr4rex) 59 | - [![augustjune](https://avatars0.githubusercontent.com/u/29662354?v=4&s=20) **augustjune**](https://github.com/augustjune) 60 | - [![Kobenko](https://avatars0.githubusercontent.com/u/21039161?v=4&s=20) **Kobenko**](https://github.com/Kobenko) 61 | - [![RawToast](https://avatars2.githubusercontent.com/u/8013000?v=4&s=20) **RawToast**](https://github.com/RawToast) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.6.8](https://github.com/scala-exercises/exercises-cats/tree/v0.6.8) (2020-08-31) 4 | 5 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.7...v0.6.8) 6 | 7 | 📈 **Dependency updates** 8 | 9 | - Update definitions, exercise-compiler, ... to 0.6.7 [\#145](https://github.com/scala-exercises/exercises-cats/pull/145) ([47erbot](https://github.com/47erbot)) 10 | - Update definitions, exercise-compiler, ... to 0.6.6 [\#143](https://github.com/scala-exercises/exercises-cats/pull/143) ([47erbot](https://github.com/47erbot)) 11 | - Update scalacheck-1-14 to 3.2.2.0 [\#142](https://github.com/scala-exercises/exercises-cats/pull/142) ([47erbot](https://github.com/47erbot)) 12 | - Update scalatest to 3.2.2 [\#141](https://github.com/scala-exercises/exercises-cats/pull/141) ([47erbot](https://github.com/47erbot)) 13 | - Update sbt-mdoc to 2.2.5 [\#140](https://github.com/scala-exercises/exercises-cats/pull/140) ([47erbot](https://github.com/47erbot)) 14 | 15 | **Closed issues:** 16 | 17 | - Eval site is still broken [\#134](https://github.com/scala-exercises/exercises-cats/issues/134) 18 | 19 | ## [v0.6.7](https://github.com/scala-exercises/exercises-cats/tree/v0.6.7) (2020-08-10) 20 | 21 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.6...v0.6.7) 22 | 23 | 📈 **Dependency updates** 24 | 25 | - Update definitions, exercise-compiler, ... to 0.6.5 [\#139](https://github.com/scala-exercises/exercises-cats/pull/139) ([47erbot](https://github.com/47erbot)) 26 | 27 | ## [v0.6.6](https://github.com/scala-exercises/exercises-cats/tree/v0.6.6) (2020-08-10) 28 | 29 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.5...v0.6.6) 30 | 31 | 📈 **Dependency updates** 32 | 33 | - Update definitions, exercise-compiler, ... to 0.6.4 [\#138](https://github.com/scala-exercises/exercises-cats/pull/138) ([47erbot](https://github.com/47erbot)) 34 | 35 | ## [v0.6.5](https://github.com/scala-exercises/exercises-cats/tree/v0.6.5) (2020-08-10) 36 | 37 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.4...v0.6.5) 38 | 39 | 📈 **Dependency updates** 40 | 41 | - Update sbt-mdoc to 2.2.4 [\#137](https://github.com/scala-exercises/exercises-cats/pull/137) ([47erbot](https://github.com/47erbot)) 42 | - Update scalacheck-1-14 to 3.2.1.0 [\#136](https://github.com/scala-exercises/exercises-cats/pull/136) ([47erbot](https://github.com/47erbot)) 43 | - Update scalatest to 3.2.1 [\#135](https://github.com/scala-exercises/exercises-cats/pull/135) ([47erbot](https://github.com/47erbot)) 44 | - Update sbt-scalafmt to 2.4.2 [\#133](https://github.com/scala-exercises/exercises-cats/pull/133) ([47erbot](https://github.com/47erbot)) 45 | - Update scalafmt-core to 2.6.4 [\#132](https://github.com/scala-exercises/exercises-cats/pull/132) ([47erbot](https://github.com/47erbot)) 46 | - Update scalafmt-core to 2.6.3 [\#129](https://github.com/scala-exercises/exercises-cats/pull/129) ([scala-steward](https://github.com/scala-steward)) 47 | - Update scalafmt-core to 2.6.2 [\#126](https://github.com/scala-exercises/exercises-cats/pull/126) ([scala-steward](https://github.com/scala-steward)) 48 | 49 | **Merged pull requests:** 50 | 51 | - Update EitherSection.scala [\#130](https://github.com/scala-exercises/exercises-cats/pull/130) ([chipmunklimo](https://github.com/chipmunklimo)) 52 | - Updated Eval Exercise to take input, spec was off as well [\#127](https://github.com/scala-exercises/exercises-cats/pull/127) ([jeremylaier](https://github.com/jeremylaier)) 53 | - Fix minor bug in code snippet example [\#69](https://github.com/scala-exercises/exercises-cats/pull/69) ([tr4rex](https://github.com/tr4rex)) 54 | 55 | ## [v0.6.4](https://github.com/scala-exercises/exercises-cats/tree/v0.6.4) (2020-06-30) 56 | 57 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.3...v0.6.4) 58 | 59 | ## [v0.6.3](https://github.com/scala-exercises/exercises-cats/tree/v0.6.3) (2020-06-29) 60 | 61 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.2...v0.6.3) 62 | 63 | 📈 **Dependency updates** 64 | 65 | - Update sbt to 1.3.13 [\#121](https://github.com/scala-exercises/exercises-cats/pull/121) ([scala-steward](https://github.com/scala-steward)) 66 | - Update sbt-mdoc to 2.2.3 [\#120](https://github.com/scala-exercises/exercises-cats/pull/120) ([scala-steward](https://github.com/scala-steward)) 67 | - Update scalafmt-core to 2.6.1 [\#119](https://github.com/scala-exercises/exercises-cats/pull/119) ([scala-steward](https://github.com/scala-steward)) 68 | - Update scalacheck-1-14 to 3.2.0.0 [\#118](https://github.com/scala-exercises/exercises-cats/pull/118) ([scala-steward](https://github.com/scala-steward)) 69 | - Update scalatest to 3.2.0 [\#117](https://github.com/scala-exercises/exercises-cats/pull/117) ([scala-steward](https://github.com/scala-steward)) 70 | - Update sbt-mdoc to 2.2.2 [\#111](https://github.com/scala-exercises/exercises-cats/pull/111) ([scala-steward](https://github.com/scala-steward)) 71 | - Update scalatest to 3.1.2 [\#103](https://github.com/scala-exercises/exercises-cats/pull/103) ([scala-steward](https://github.com/scala-steward)) 72 | 73 | **Merged pull requests:** 74 | 75 | - Update definitions, exercise-compiler, ... to 0.6.3 [\#122](https://github.com/scala-exercises/exercises-cats/pull/122) ([scala-steward](https://github.com/scala-steward)) 76 | - Update scalafmt-core to 2.6.0 [\#115](https://github.com/scala-exercises/exercises-cats/pull/115) ([BenFradet](https://github.com/BenFradet)) 77 | - Update scalafmt-core to 2.5.3 [\#113](https://github.com/scala-exercises/exercises-cats/pull/113) ([BenFradet](https://github.com/BenFradet)) 78 | - Prepare repository for next release and SBT build improvements [\#110](https://github.com/scala-exercises/exercises-cats/pull/110) ([juanpedromoreno](https://github.com/juanpedromoreno)) 79 | - Update sbt to 1.3.12 [\#109](https://github.com/scala-exercises/exercises-cats/pull/109) ([scala-steward](https://github.com/scala-steward)) 80 | - Replace deprecated kind-projector syntax \[use '\*' instead of '?'\] [\#106](https://github.com/scala-exercises/exercises-cats/pull/106) ([Samehadar](https://github.com/Samehadar)) 81 | - Add a link to the documentation referenced [\#104](https://github.com/scala-exercises/exercises-cats/pull/104) ([dmarticus](https://github.com/dmarticus)) 82 | - Add doc section about other monad tranformers [\#68](https://github.com/scala-exercises/exercises-cats/pull/68) ([augustjune](https://github.com/augustjune)) 83 | 84 | ## [v0.6.2](https://github.com/scala-exercises/exercises-cats/tree/v0.6.2) (2020-04-27) 85 | 86 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.1...v0.6.2) 87 | 88 | ## [v0.6.1](https://github.com/scala-exercises/exercises-cats/tree/v0.6.1) (2020-04-27) 89 | 90 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.6.0...v0.6.1) 91 | 92 | **Merged pull requests:** 93 | 94 | - Compile when Publish [\#99](https://github.com/scala-exercises/exercises-cats/pull/99) ([juanpedromoreno](https://github.com/juanpedromoreno)) 95 | 96 | ## [v0.6.0](https://github.com/scala-exercises/exercises-cats/tree/v0.6.0) (2020-04-24) 97 | 98 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/v0.4.0...v0.6.0) 99 | 100 | **Closed issues:** 101 | 102 | - First example in Semigroups does not work on current web-site [\#65](https://github.com/scala-exercises/exercises-cats/issues/65) 103 | - First example in Semigroups does not work on current web-site [\#64](https://github.com/scala-exercises/exercises-cats/issues/64) 104 | 105 | **Merged pull requests:** 106 | 107 | - Updates Project [\#92](https://github.com/scala-exercises/exercises-cats/pull/92) ([juanpedromoreno](https://github.com/juanpedromoreno)) 108 | - Update sbt to 1.3.10 [\#90](https://github.com/scala-exercises/exercises-cats/pull/90) ([scala-steward](https://github.com/scala-steward)) 109 | - Update sbt to 1.3.9 [\#89](https://github.com/scala-exercises/exercises-cats/pull/89) ([scala-steward](https://github.com/scala-steward)) 110 | - Added OptionT exercises [\#88](https://github.com/scala-exercises/exercises-cats/pull/88) ([paualarco](https://github.com/paualarco)) 111 | - Added Eval exercises [\#87](https://github.com/scala-exercises/exercises-cats/pull/87) ([paualarco](https://github.com/paualarco)) 112 | - Update scalacheck-shapeless\_1.14 to 1.2.5 [\#86](https://github.com/scala-exercises/exercises-cats/pull/86) ([scala-steward](https://github.com/scala-steward)) 113 | - Update scalatest to 3.1.1 [\#85](https://github.com/scala-exercises/exercises-cats/pull/85) ([scala-steward](https://github.com/scala-steward)) 114 | - Mergify: configuration update [\#84](https://github.com/scala-exercises/exercises-cats/pull/84) ([angoglez](https://github.com/angoglez)) 115 | - null narrative references to |@| removed [\#83](https://github.com/scala-exercises/exercises-cats/pull/83) ([mayonesa](https://github.com/mayonesa)) 116 | - Update scalacheck-shapeless\_1.14 to 1.2.4 [\#82](https://github.com/scala-exercises/exercises-cats/pull/82) ([scala-steward](https://github.com/scala-steward)) 117 | - Update sbt to 1.3.8 [\#81](https://github.com/scala-exercises/exercises-cats/pull/81) ([scala-steward](https://github.com/scala-steward)) 118 | - Update to Scala 2.13.1 [\#80](https://github.com/scala-exercises/exercises-cats/pull/80) ([kiroco12](https://github.com/kiroco12)) 119 | - Rewording a sentence [\#77](https://github.com/scala-exercises/exercises-cats/pull/77) ([TobiasRoland](https://github.com/TobiasRoland)) 120 | - Update scalacheck to 1.14.3 [\#75](https://github.com/scala-exercises/exercises-cats/pull/75) ([scala-steward](https://github.com/scala-steward)) 121 | - ScalaCheck Update 3.1.0 [\#73](https://github.com/scala-exercises/exercises-cats/pull/73) ([kiroco12](https://github.com/kiroco12)) 122 | - Exercises Update to Scala 2.12.10 [\#72](https://github.com/scala-exercises/exercises-cats/pull/72) ([kiroco12](https://github.com/kiroco12)) 123 | - Update README.md [\#71](https://github.com/scala-exercises/exercises-cats/pull/71) ([jkmcclellan](https://github.com/jkmcclellan)) 124 | - Omit explicit parameter types of anonymous functions [\#66](https://github.com/scala-exercises/exercises-cats/pull/66) ([Kobenko](https://github.com/Kobenko)) 125 | - Break apart a set of five tests [\#61](https://github.com/scala-exercises/exercises-cats/pull/61) ([doub1ejack](https://github.com/doub1ejack)) 126 | - Updating sbt org policies [\#58](https://github.com/scala-exercises/exercises-cats/pull/58) ([dominv](https://github.com/dominv)) 127 | - Updating libraries versions [\#57](https://github.com/scala-exercises/exercises-cats/pull/57) ([dominv](https://github.com/dominv)) 128 | - Update cats to 0.9.0 and change Xor to Either [\#55](https://github.com/scala-exercises/exercises-cats/pull/55) ([aeons](https://github.com/aeons)) 129 | - Update .travis.yml [\#50](https://github.com/scala-exercises/exercises-cats/pull/50) ([juanpedromoreno](https://github.com/juanpedromoreno)) 130 | 131 | ## [v0.4.0](https://github.com/scala-exercises/exercises-cats/tree/v0.4.0) (2017-03-28) 132 | 133 | [Full Changelog](https://github.com/scala-exercises/exercises-cats/compare/cf5a25aaaf380c0d0337ee9fc46e34b6e225ecc9...v0.4.0) 134 | 135 | 🚀 **Features** 136 | 137 | - Show better errors, i.e. line numbers [\#27](https://github.com/scala-exercises/exercises-cats/issues/27) 138 | 139 | **Closed issues:** 140 | 141 | - Cats version used in examples is outdated [\#39](https://github.com/scala-exercises/exercises-cats/issues/39) 142 | - Publish snapshot artifact from master with each successful build [\#9](https://github.com/scala-exercises/exercises-cats/issues/9) 143 | 144 | **Merged pull requests:** 145 | 146 | - Fixes missing cats Dependency [\#56](https://github.com/scala-exercises/exercises-cats/pull/56) ([juanpedromoreno](https://github.com/juanpedromoreno)) 147 | - Integrates sbt-org-policies plugin [\#52](https://github.com/scala-exercises/exercises-cats/pull/52) ([juanpedromoreno](https://github.com/juanpedromoreno)) 148 | - Update Foldable.scala [\#49](https://github.com/scala-exercises/exercises-cats/pull/49) ([lloydmeta](https://github.com/lloydmeta)) 149 | - Updates PGP keys [\#47](https://github.com/scala-exercises/exercises-cats/pull/47) ([juanpedromoreno](https://github.com/juanpedromoreno)) 150 | - SE-597 - Upgrade Libs [\#45](https://github.com/scala-exercises/exercises-cats/pull/45) ([juanpedromoreno](https://github.com/juanpedromoreno)) 151 | - Fix typos in Validated [\#44](https://github.com/scala-exercises/exercises-cats/pull/44) ([ches](https://github.com/ches)) 152 | - Added README to content repository [\#43](https://github.com/scala-exercises/exercises-cats/pull/43) ([jdesiloniz](https://github.com/jdesiloniz)) 153 | - Include logo in content library [\#42](https://github.com/scala-exercises/exercises-cats/pull/42) ([jdesiloniz](https://github.com/jdesiloniz)) 154 | - Bumps to 0.2.4-SNAPSHOT version [\#41](https://github.com/scala-exercises/exercises-cats/pull/41) ([juanpedromoreno](https://github.com/juanpedromoreno)) 155 | - Apply: `addTwo` wasn't being used [\#40](https://github.com/scala-exercises/exercises-cats/pull/40) ([mttkay](https://github.com/mttkay)) 156 | - JP - Bumps version [\#38](https://github.com/scala-exercises/exercises-cats/pull/38) ([juanpedromoreno](https://github.com/juanpedromoreno)) 157 | - Add missed import in Apply [\#35](https://github.com/scala-exercises/exercises-cats/pull/35) ([ikhoon](https://github.com/ikhoon)) 158 | - JP - SE-510 DRY Test.scala [\#34](https://github.com/scala-exercises/exercises-cats/pull/34) ([juanpedromoreno](https://github.com/juanpedromoreno)) 159 | - Add return type in Traverse example [\#33](https://github.com/scala-exercises/exercises-cats/pull/33) ([richardmiller](https://github.com/richardmiller)) 160 | - Provide `Some` for user on `composingSemigroups` [\#32](https://github.com/scala-exercises/exercises-cats/pull/32) ([metasim](https://github.com/metasim)) 161 | - Updates Cats library description [\#31](https://github.com/scala-exercises/exercises-cats/pull/31) ([raulraja](https://github.com/raulraja)) 162 | - Fix 404 link [\#30](https://github.com/scala-exercises/exercises-cats/pull/30) ([Thangiee](https://github.com/Thangiee)) 163 | - Bump new minor release [\#29](https://github.com/scala-exercises/exercises-cats/pull/29) ([ghost](https://github.com/ghost)) 164 | - Bump next version [\#28](https://github.com/scala-exercises/exercises-cats/pull/28) ([ghost](https://github.com/ghost)) 165 | - Use toolchain with the same version as the content [\#26](https://github.com/scala-exercises/exercises-cats/pull/26) ([ghost](https://github.com/ghost)) 166 | - Increment version number for including a timestamp in the library metadata [\#25](https://github.com/scala-exercises/exercises-cats/pull/25) ([ghost](https://github.com/ghost)) 167 | - Update plugin version [\#24](https://github.com/scala-exercises/exercises-cats/pull/24) ([ghost](https://github.com/ghost)) 168 | - Depend on 0.1.x runtime and definitions [\#23](https://github.com/scala-exercises/exercises-cats/pull/23) ([ghost](https://github.com/ghost)) 169 | - Bump first release [\#22](https://github.com/scala-exercises/exercises-cats/pull/22) ([ghost](https://github.com/ghost)) 170 | - Extract implicits from the exercise bodies [\#21](https://github.com/scala-exercises/exercises-cats/pull/21) ([ghost](https://github.com/ghost)) 171 | - Empty commit in order to release a snapshot [\#20](https://github.com/scala-exercises/exercises-cats/pull/20) ([ghost](https://github.com/ghost)) 172 | - Fix Validated text [\#19](https://github.com/scala-exercises/exercises-cats/pull/19) ([sergiohgz](https://github.com/sergiohgz)) 173 | - Remove legacy generated file [\#18](https://github.com/scala-exercises/exercises-cats/pull/18) ([andyscott](https://github.com/andyscott)) 174 | - Deploy after successful builds in master [\#17](https://github.com/scala-exercises/exercises-cats/pull/17) ([ghost](https://github.com/ghost)) 175 | - Change comment [\#16](https://github.com/scala-exercises/exercises-cats/pull/16) ([yilinwei](https://github.com/yilinwei)) 176 | - Skip working tree cleanup when doing a deploy [\#15](https://github.com/scala-exercises/exercises-cats/pull/15) ([ghost](https://github.com/ghost)) 177 | - Do a manual compilation before publish [\#14](https://github.com/scala-exercises/exercises-cats/pull/14) ([ghost](https://github.com/ghost)) 178 | - Automatically deploy master branch after successful build [\#13](https://github.com/scala-exercises/exercises-cats/pull/13) ([ghost](https://github.com/ghost)) 179 | - Revert "Publish from Travis CI after successful builds" [\#12](https://github.com/scala-exercises/exercises-cats/pull/12) ([raulraja](https://github.com/raulraja)) 180 | - Publish from Travis CI after successful builds [\#11](https://github.com/scala-exercises/exercises-cats/pull/11) ([ghost](https://github.com/ghost)) 181 | - Fix link [\#10](https://github.com/scala-exercises/exercises-cats/pull/10) ([ghost](https://github.com/ghost)) 182 | - Put Semigroup section before Monoid [\#8](https://github.com/scala-exercises/exercises-cats/pull/8) ([ghost](https://github.com/ghost)) 183 | - Change library color [\#7](https://github.com/scala-exercises/exercises-cats/pull/7) ([rafaparadela](https://github.com/rafaparadela)) 184 | - Add library repo metadata [\#5](https://github.com/scala-exercises/exercises-cats/pull/5) ([ghost](https://github.com/ghost)) 185 | - Rename artifact [\#4](https://github.com/scala-exercises/exercises-cats/pull/4) ([ghost](https://github.com/ghost)) 186 | - Use the Xor type for the answers [\#3](https://github.com/scala-exercises/exercises-cats/pull/3) ([ghost](https://github.com/ghost)) 187 | - Rafa add distribution settings [\#2](https://github.com/scala-exercises/exercises-cats/pull/2) ([rafaparadela](https://github.com/rafaparadela)) 188 | - Update CI config [\#1](https://github.com/scala-exercises/exercises-cats/pull/1) ([ghost](https://github.com/ghost)) 189 | 190 | 191 | 192 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 193 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Code of Conduct 6 | 7 | We are committed to providing a friendly, safe and welcoming 8 | environment for all, regardless of level of experience, gender, gender 9 | identity and expression, sexual orientation, disability, personal 10 | appearance, body size, race, ethnicity, age, religion, nationality, or 11 | other such characteristics. 12 | 13 | Everyone is expected to follow the 14 | [Scala Code of Conduct](https://www.scala-lang.org/conduct/) when 15 | discussing the project on the available communication channels. If you 16 | are being harassed, please contact us immediately so that we can 17 | support you. 18 | 19 | ## Moderation 20 | 21 | For any questions, concerns, or moderation requests please contact a 22 | [member of the project](AUTHORS.md#maintainers). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Contributing 6 | 7 | Discussion around _exercises-cats_ happens in the [GitHub issues](https://github.com/scala-exercises/exercises-cats/issues) and [pull requests](https://github.com/scala-exercises/exercises-cats/pulls). 8 | 9 | Feel free to open an issue if you notice a bug, have an idea for a feature, or have a question about 10 | the code. Pull requests are also welcome. 11 | 12 | People are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md) when discussing _exercises-cats_ on the Github page or other venues. 13 | 14 | If you are being harassed, please contact one of [us](AUTHORS.md#maintainers) immediately so that we can support you. In case you cannot get in touch with us please write an email to [47 Degrees Open Source](mailto:hello@47deg.com). 15 | 16 | ## How can I help? 17 | 18 | _exercises-cats_ follows a standard [fork and pull](https://help.github.com/articles/using-pull-requests/) model for contributions via GitHub pull requests. 19 | 20 | The process is simple: 21 | 22 | 1. Find something you want to work on 23 | 2. Let us know you are working on it via GitHub issues/pull requests 24 | 3. Implement your contribution 25 | 4. Write tests 26 | 5. Update the documentation 27 | 6. Submit pull request 28 | 29 | You will be automatically included in the [AUTHORS.md](AUTHORS.md#contributors) file as contributor in the next release. 30 | 31 | If you encounter any confusion or frustration during the contribution process, please create a GitHub issue and we'll do our best to improve the process. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (C) 2016-2020 47 Degrees Open Source 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | exercises-cats 6 | 7 | Copyright (c) 2016-2020 47 Degrees Open Source. All rights reserved. 8 | 9 | Licensed under Apache-2.0. See [LICENSE](LICENSE.md) for terms. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Exercises - Cats library 2 | 3 | This repository hosts a content library for the [Scala Exercises](https://www.scala-exercises.org/) platform that includes interactive exercises related to the [Cats library](https://github.com/typelevel/cats) by Typelevel. 4 | 5 | ## About Scala Exercises 6 | 7 | "Scala Exercises" brings exercises for the Stdlib, Cats, Shapeless, and many other great libraries for Scala to your browser. It offers hundreds of solvable exercises organized into several categories covering the basics of the Scala language and its most important libraries. 8 | 9 | Scala Exercises is available at [scala-exercises.org](https://scala-exercises.org). 10 | 11 | ## Contributing 12 | 13 | Contributions are welcome! Please join our [Gitter channel](https://gitter.im/scala-exercises/scala-exercises) 14 | to get involved, or visit our [GitHub site](https://github.com/scala-exercises). 15 | 16 | ## License 17 | 18 | Copyright (C) 2015-2020 47 Degrees, LLC. 19 | Reactive, scalable software solutions. 20 | http://47deg.com 21 | hello@47deg.com 22 | 23 | Licensed under the Apache License, Version 2.0 (the "License"); 24 | you may not use this file except in compliance with the License. 25 | You may obtain a copy of the License at 26 | 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | Unless required by applicable law or agreed to in writing, software 30 | distributed under the License is distributed on an "AS IS" BASIS, 31 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | See the License for the specific language governing permissions and 33 | limitations under the License. 34 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.jsuereth.sbtpgp.PgpKeys.publishSigned 2 | 3 | ThisBuild / organization := "org.scala-exercises" 4 | ThisBuild / githubOrganization := "47degrees" 5 | ThisBuild / scalaVersion := "2.13.10" 6 | 7 | ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % "always" 8 | 9 | // This is required by the exercises compiler: 10 | publishLocal := (publishLocal dependsOn compile).value 11 | publishSigned := (publishSigned dependsOn compile).value 12 | 13 | addCommandAlias("ci-test", "scalafmtCheckAll; scalafmtSbtCheck; test") 14 | addCommandAlias("ci-docs", "github; documentation/mdoc; headerCreateAll") 15 | addCommandAlias("ci-publish", "github; ci-release") 16 | 17 | lazy val exercises = (project in file(".")) 18 | .settings(name := "exercises-cats") 19 | .settings( 20 | libraryDependencies ++= Seq( 21 | "org.scala-exercises" %% "exercise-compiler" % "0.7.1", 22 | "org.scala-exercises" %% "definitions" % "0.7.1", 23 | "org.typelevel" %% "cats-core" % "2.9.0", 24 | "com.chuusai" %% "shapeless" % "2.3.10", 25 | "org.scalatest" %% "scalatest" % "3.2.14", 26 | "org.scalacheck" %% "scalacheck" % "1.17.0", 27 | "org.scalatestplus" %% "scalacheck-1-14" % "3.2.2.0", 28 | "com.github.alexarchambault" %% "scalacheck-shapeless_1.15" % "1.3.0" 29 | ), 30 | addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full) 31 | ) 32 | .enablePlugins(ExerciseCompilerPlugin) 33 | 34 | lazy val documentation = project 35 | .settings(mdocOut := file(".")) 36 | .settings(publish / skip := true) 37 | .enablePlugins(MdocPlugin) 38 | -------------------------------------------------------------------------------- /docs/AUTHORS.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Authors 6 | 7 | ## Maintainers 8 | 9 | The maintainers of the project are: 10 | 11 | @COLLABORATORS@ 12 | 13 | ## Contributors 14 | 15 | These are the people that have contributed to the _@NAME@_ project: 16 | 17 | @CONTRIBUTORS@ -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Code of Conduct 6 | 7 | We are committed to providing a friendly, safe and welcoming 8 | environment for all, regardless of level of experience, gender, gender 9 | identity and expression, sexual orientation, disability, personal 10 | appearance, body size, race, ethnicity, age, religion, nationality, or 11 | other such characteristics. 12 | 13 | Everyone is expected to follow the 14 | [Scala Code of Conduct](https://www.scala-lang.org/conduct/) when 15 | discussing the project on the available communication channels. If you 16 | are being harassed, please contact us immediately so that we can 17 | support you. 18 | 19 | ## Moderation 20 | 21 | For any questions, concerns, or moderation requests please contact a 22 | [member of the project](AUTHORS.md#maintainers). -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | # Contributing 6 | 7 | Discussion around _@NAME@_ happens in the [GitHub issues](https://github.com/@REPO@/issues) and [pull requests](https://github.com/@REPO@/pulls). 8 | 9 | Feel free to open an issue if you notice a bug, have an idea for a feature, or have a question about 10 | the code. Pull requests are also welcome. 11 | 12 | People are expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md) when discussing _@NAME@_ on the Github page or other venues. 13 | 14 | If you are being harassed, please contact one of [us](AUTHORS.md#maintainers) immediately so that we can support you. In case you cannot get in touch with us please write an email to [@ORG_NAME@](mailto:@ORG_EMAIL@). 15 | 16 | ## How can I help? 17 | 18 | _@NAME@_ follows a standard [fork and pull](https://help.github.com/articles/using-pull-requests/) model for contributions via GitHub pull requests. 19 | 20 | The process is simple: 21 | 22 | 1. Find something you want to work on 23 | 2. Let us know you are working on it via GitHub issues/pull requests 24 | 3. Implement your contribution 25 | 4. Write tests 26 | 5. Update the documentation 27 | 6. Submit pull request 28 | 29 | You will be automatically included in the [AUTHORS.md](AUTHORS.md#contributors) file as contributor in the next release. 30 | 31 | If you encounter any confusion or frustration during the contribution process, please create a GitHub issue and we'll do our best to improve the process. -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (C) @YEAR_RANGE@ @COPYRIGHT_OWNER@ 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /docs/NOTICE.md: -------------------------------------------------------------------------------- 1 | [comment]: <> (Don't edit this file!) 2 | [comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) 3 | [comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) 4 | 5 | @NAME@ 6 | 7 | Copyright (c) @YEAR_RANGE@ @ORG_NAME@. All rights reserved. 8 | 9 | Licensed under @LICENSE@. See [LICENSE](LICENSE.md) for terms. -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.8.2 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-exercises" % "sbt-exercise" % "0.7.1") 2 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") 3 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") 4 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") 5 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.9.0") 6 | addSbtPlugin("com.alejandrohdezma" % "sbt-github" % "0.11.8") 7 | addSbtPlugin("com.alejandrohdezma" % "sbt-github-header" % "0.11.8") 8 | addSbtPlugin("com.alejandrohdezma" % "sbt-github-mdoc" % "0.11.8") 9 | addSbtPlugin("com.alejandrohdezma" % "sbt-remove-test-from-pom" % "0.1.0") 10 | -------------------------------------------------------------------------------- /src/main/resources/cats.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cats 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Applicative.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats._ 23 | import cats.implicits._ 24 | 25 | /** 26 | * `Applicative` extends `Apply` by adding a single method, `pure`: 27 | * 28 | * {{{ 29 | * def pure[A](x: A): F[A] 30 | * }}} 31 | * 32 | * @param name 33 | * applicative 34 | */ 35 | object ApplicativeSection 36 | extends AnyFlatSpec 37 | with Matchers 38 | with org.scalaexercises.definitions.Section { 39 | 40 | /** 41 | * This method takes any value and returns the value in the context of the functor. For many 42 | * familiar functors, how to do this is obvious. For `Option`, the `pure` operation wraps the 43 | * value in `Some`. For `List`, the `pure` operation returns a single element `List`: 44 | */ 45 | def pureMethod(res0: Option[Int], res1: List[Int]) = { 46 | import cats._ 47 | import cats.implicits._ 48 | 49 | Applicative[Option].pure(1) should be(res0) 50 | Applicative[List].pure(1) should be(res1) 51 | } 52 | 53 | /** 54 | * Like `Functor` and `Apply`, `Applicative` functors also compose naturally with each other. When 55 | * you compose one `Applicative` with another, the resulting `pure` operation will lift the passed 56 | * value into one context, and the result into the other context: 57 | */ 58 | def applicativeComposition(res0: List[Option[Int]]) = 59 | (Applicative[List] compose Applicative[Option]).pure(1) should be(res0) 60 | 61 | /** 62 | * =Applicative Functors & Monads= 63 | * 64 | * `Applicative` is a generalization of `Monad`, allowing expression of effectful computations in 65 | * a pure functional way. 66 | * 67 | * `Applicative` is generally preferred to `Monad` when the structure of a computation is fixed a 68 | * priori. That makes it possible to perform certain kinds of static analysis on applicative 69 | * values. 70 | */ 71 | def applicativesAndMonads(res0: Option[Int], res1: Option[Int]) = { 72 | Monad[Option].pure(1) should be(res0) 73 | Applicative[Option].pure(1) should be(res1) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Apply.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import ApplyHelpers._ 22 | 23 | import cats._ 24 | import cats.implicits._ 25 | 26 | /** 27 | * `Apply` extends the `Functor` type class (which features the familiar `map` function) with a new 28 | * function `ap`. The `ap` function is similar to `map` in that we are transforming a value in a 29 | * context (a context being the `F` in `F[A]`; a context can be `Option`, `List` or `Future` for 30 | * example). However, the difference between `ap` and `map` is that for `ap` the function that takes 31 | * care of the transformation is of type `F[A => B]`, whereas for `map` it is `A => B`: 32 | * 33 | * Here are the implementations of `Apply` for the `Option` and `List` types: 34 | * {{{ 35 | * import cats._ 36 | * 37 | * implicit val optionApply: Apply[Option] = new Apply[Option] { 38 | * def ap[A, B](f: Option[A => B])(fa: Option[A]): Option[B] = 39 | * fa.flatMap (a => f.map (ff => ff(a))) 40 | * 41 | * def map[A,B](fa: Option[A])(f: A => B): Option[B] = fa map f 42 | * 43 | * def product[A, B](fa: Option[A], fb: Option[B]): Option[(A, B)] = 44 | * fa.flatMap(a => fb.map(b => (a, b))) 45 | * } 46 | * 47 | * implicit val listApply: Apply[List] = new Apply[List] { 48 | * def ap[A, B](f: List[A => B])(fa: List[A]): List[B] = 49 | * fa.flatMap (a => f.map (ff => ff(a))) 50 | * 51 | * def map[A,B](fa: List[A])(f: A => B): List[B] = fa map f 52 | * 53 | * def product[A, B](fa: List[A], fb: List[B]): List[(A, B)] = 54 | * fa.zip(fb) 55 | * } 56 | * }}} 57 | * 58 | * @param name 59 | * apply 60 | */ 61 | object ApplySection extends AnyFlatSpec with Matchers with org.scalaexercises.definitions.Section { 62 | 63 | /** 64 | * =map= 65 | * 66 | * Since `Apply` extends `Functor`, we can use the `map` method from `Functor`: 67 | */ 68 | def applyExtendsFunctor(res0: Option[String], res1: Option[Int], res2: Option[Int]) = { 69 | import cats.implicits._ 70 | 71 | val intToString: Int => String = _.toString 72 | val double: Int => Int = _ * 2 73 | val addTwo: Int => Int = _ + 2 74 | 75 | Apply[Option].map(Some(1))(intToString) should be(res0) 76 | Apply[Option].map(Some(1))(double) should be(res1) 77 | Apply[Option].map(None)(addTwo) should be(res2) 78 | } 79 | 80 | /** 81 | * =compose= 82 | * 83 | * And like functors, `Apply` instances also compose: 84 | */ 85 | def applyComposes(res0: List[Option[Int]]) = { 86 | val listOpt = Apply[List] compose Apply[Option] 87 | val plusOne = (x: Int) => x + 1 88 | listOpt.ap(List(Some(plusOne)))(List(Some(1), None, Some(3))) should be(res0) 89 | } 90 | 91 | /** 92 | * =ap= 93 | * 94 | * The `ap` method is a method that `Functor` does not have: 95 | */ 96 | def applyAp( 97 | res0: Option[String], 98 | res1: Option[Int], 99 | res2: Option[Int], 100 | res3: Option[Int], 101 | res4: Option[Int] 102 | ) = { 103 | Apply[Option].ap(Some(intToString))(Some(1)) should be(res0) 104 | Apply[Option].ap(Some(double))(Some(1)) should be(res1) 105 | Apply[Option].ap(Some(double))(None) should be(res2) 106 | Apply[Option].ap(None)(Some(1)) should be(res3) 107 | Apply[Option].ap(None)(None) should be(res4) 108 | } 109 | 110 | /** 111 | * =ap2, ap3, etc= 112 | * 113 | * `Apply` also offers variants of `ap`. The functions `apN` (for `N` between `2` and `22`) accept 114 | * `N` arguments where `ap` accepts `1`. 115 | * 116 | * Note that if any of the arguments of this example is `None`, the final result is `None` as 117 | * well. The effects of the context we are operating on are carried through the entire 118 | * computation: 119 | */ 120 | def applyApn(res0: Option[Int], res1: Option[Int], res2: Option[Int]) = { 121 | val addArity2 = (a: Int, b: Int) => a + b 122 | Apply[Option].ap2(Some(addArity2))(Some(1), Some(2)) should be(res0) 123 | Apply[Option].ap2(Some(addArity2))(Some(1), None) should be(res1) 124 | 125 | val addArity3 = (a: Int, b: Int, c: Int) => a + b + c 126 | Apply[Option].ap3(Some(addArity3))(Some(1), Some(2), Some(3)) should be(res2) 127 | } 128 | 129 | /** 130 | * =map2, map3, etc= 131 | * 132 | * Similarly, `mapN` functions are available: 133 | */ 134 | def applyMapn(res0: Option[Int], res1: Option[Int]) = { 135 | Apply[Option].map2(Some(1), Some(2))(addArity2) should be(res0) 136 | 137 | Apply[Option].map3(Some(1), Some(2), Some(3))(addArity3) should be(res1) 138 | } 139 | 140 | /** 141 | * =tuple2, tuple3, etc= 142 | * 143 | * Similarly, `tupleN` functions are available: 144 | */ 145 | def applyTuplen(res0: Option[(Int, Int)], res1: Option[(Int, Int, Int)]) = { 146 | Apply[Option].tuple2(Some(1), Some(2)) should be(res0) 147 | Apply[Option].tuple3(Some(1), Some(2), Some(3)) should be(res1) 148 | } 149 | 150 | /** 151 | * =apply builder syntax= 152 | * 153 | * In order to use functions `apN`, `mapN` and `tupleN` *infix*, import `cats.implicits._`. 154 | */ 155 | def applyBuilderSyntax( 156 | res0: Option[Int], 157 | res1: Option[Int], 158 | res2: Option[Int], 159 | res3: Option[Int], 160 | res4: Option[(Int, Int)], 161 | res5: Option[(Int, Int, Int)] 162 | ) = { 163 | import cats.implicits._ 164 | val option2 = (Option(1), Option(2)) 165 | val option3 = (option2._1, option2._2, Option.empty[Int]) 166 | 167 | option2 mapN addArity2 should be(res0) 168 | option3 mapN addArity3 should be(res1) 169 | 170 | option2 apWith Some(addArity2) should be(res2) 171 | option3 apWith Some(addArity3) should be(res3) 172 | 173 | option2.tupled should be(res4) 174 | option3.tupled should be(res5) 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/scala/catslib/ApplyHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | object ApplyHelpers { 20 | val intToString: Int => String = _.toString 21 | val double: Int => Int = _ * 2 22 | val addTwo: Int => Int = _ + 2 23 | val addArity2 = (a: Int, b: Int) => a + b 24 | val addArity3 = (a: Int, b: Int, c: Int) => a + b + c 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/catslib/CatsLibrary.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | /** 20 | * Cats is a library which provides abstractions for functional programming in the Scala programming 21 | * language. 22 | * 23 | * @param name 24 | * cats 25 | */ 26 | object CatsLibrary extends org.scalaexercises.definitions.Library { 27 | override def owner = "scala-exercises" 28 | override def repository = "exercises-cats" 29 | 30 | override def color = Some("#5B5988") 31 | 32 | override def sections = 33 | List( 34 | SemigroupSection, 35 | MonoidSection, 36 | FunctorSection, 37 | ApplySection, 38 | ApplicativeSection, 39 | MonadSection, 40 | FoldableSection, 41 | TraverseSection, 42 | IdentitySection, 43 | EitherSection, 44 | ValidatedSection, 45 | EvalSection 46 | ) 47 | 48 | override def logoPath = "cats" 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/catslib/EitherSection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats.implicits._ 20 | 21 | import org.scalatest.matchers.should.Matchers 22 | import org.scalatest.flatspec.AnyFlatSpec 23 | 24 | object EitherStyle { 25 | def parse(s: String): Either[NumberFormatException, Int] = 26 | if (s.matches("-?[0-9]+")) Either.right(s.toInt) 27 | else Either.left(new NumberFormatException(s"$s is not a valid integer.")) 28 | 29 | def reciprocal(i: Int): Either[IllegalArgumentException, Double] = 30 | if (i == 0) Either.left(new IllegalArgumentException("Cannot take reciprocal of 0.")) 31 | else Either.right(1.0 / i) 32 | 33 | def stringify(d: Double): String = d.toString 34 | 35 | def magic(s: String): Either[Exception, String] = 36 | parse(s).flatMap(reciprocal).map(stringify) 37 | } 38 | 39 | object EitherStyleWithAdts { 40 | sealed abstract class Error 41 | final case class NotANumber(string: String) extends Error 42 | final case object NoZeroReciprocal extends Error 43 | 44 | def parse(s: String): Either[Error, Int] = 45 | if (s.matches("-?[0-9]+")) Either.right(s.toInt) 46 | else Either.left(NotANumber(s)) 47 | 48 | def reciprocal(i: Int): Either[Error, Double] = 49 | if (i == 0) Either.left(NoZeroReciprocal) 50 | else Either.right(1.0 / i) 51 | 52 | def stringify(d: Double): String = d.toString 53 | 54 | def magic(s: String): Either[Error, String] = 55 | parse(s).flatMap(reciprocal).map(stringify) 56 | } 57 | 58 | /** 59 | * In day-to-day programming, it is fairly common to find ourselves writing functions that can fail. 60 | * For instance, querying a service may result in a connection issue, or some unexpected JSON 61 | * response. 62 | * 63 | * To communicate these errors it has become common practice to throw exceptions. However, 64 | * exceptions are not tracked in any way, shape, or form by the Scala compiler. To see what kind of 65 | * exceptions (if any) a function may throw, we have to dig through the source code. Then to handle 66 | * these exceptions, we have to make sure we catch them at the call site. This all becomes even more 67 | * unwieldy when we try to compose exception-throwing procedures. 68 | * 69 | * {{{ 70 | * val throwsSomeStuff: Int => Double = ??? 71 | * 72 | * val throwsOtherThings: Double => String = ??? 73 | * 74 | * val moreThrowing: String => List[Char] = ??? 75 | * 76 | * val magic = throwsSomeStuff.andThen(throwsOtherThings).andThen(moreThrowing) 77 | * }}} 78 | * 79 | * Assume we happily throw exceptions in our code. Looking at the types, any of those functions can 80 | * throw any number of exceptions, we don't know. When we compose, exceptions from any of the 81 | * constituent functions can be thrown. Moreover, they may throw the same kind of exception (e.g. 82 | * `IllegalArgumentException`) and thus it gets tricky tracking exactly where that exception came 83 | * from. 84 | * 85 | * How then do we communicate an error? By making it explicit in the data type we return. 86 | * 87 | * =`Either` vs `Validated`= 88 | * 89 | * In general, `Validated` is used to accumulate errors, while `Either` is used to short-circuit a 90 | * computation upon the first error. For more information, see the 91 | * [[https://typelevel.org/cats/datatypes/validated.html#validated-vs-either Validated vs Either]] 92 | * section of the `Validated` documentation. 93 | * 94 | * @param name 95 | * either 96 | */ 97 | object EitherSection extends AnyFlatSpec with Matchers with org.scalaexercises.definitions.Section { 98 | 99 | /** 100 | * More often than not we want to just bias towards one side and call it a day - by convention, 101 | * the right side is most often chosen. 102 | */ 103 | def eitherMapRightBias(res0: Either[String, Int], res1: Either[String, Int]) = { 104 | 105 | val right: Either[String, Int] = Either.right(5) 106 | right.map(_ + 1) should be(res0) 107 | 108 | val left: Either[String, Int] = Either.left("Something went wrong") 109 | left.map(_ + 1) should be(res1) 110 | } 111 | 112 | /** 113 | * Because `Either` is right-biased, it is possible to define a `Monad` instance for it. 114 | * 115 | * Since we only ever want the computation to continue in the case of `Right` (as captured by the 116 | * right-bias nature), we fix the left type parameter and leave the right one free. 117 | * 118 | * {{{ 119 | * import cats.implicits._ 120 | * import cats.Monad 121 | * 122 | * implicit def eitherMonad[Err]: Monad[Either[Err, *]] = 123 | * new Monad[Either[Err, *]] { 124 | * def flatMap[A, B](fa: Either[Err, A])(f: A => Either[Err, B]): Either[Err, B] = 125 | * fa.flatMap(f) 126 | * 127 | * def pure[A](x: A): Either[Err, A] = Either.right(x) 128 | * } 129 | * }}} 130 | * 131 | * So the `flatMap` method is right-biased: 132 | */ 133 | def eitherMonad(res0: Either[String, Int], res1: Either[String, Int]) = { 134 | 135 | val right: Either[String, Int] = Either.right(5) 136 | right.flatMap(x => Either.right(x + 1)) should be(res0) 137 | 138 | val left: Either[String, Int] = Either.left("Something went wrong") 139 | left.flatMap(x => Either.right(x + 1)) should be(res1) 140 | } 141 | 142 | /** 143 | * =Using `Either` instead of exceptions= 144 | * 145 | * As a running example, we will have a series of functions that will parse a string into an 146 | * integer, take the reciprocal, and then turn the reciprocal into a string. 147 | * 148 | * In exception-throwing code, we would have something like this: 149 | * 150 | * {{{ 151 | * object ExceptionStyle { 152 | * def parse(s: String): Int = 153 | * if (s.matches("-?[0-9]+")) s.toInt 154 | * else throw new NumberFormatException(s"${s} is not a valid integer.") 155 | * 156 | * def reciprocal(i: Int): Double = 157 | * if (i == 0) throw new IllegalArgumentException("Cannot take reciprocal of 0.") 158 | * else 1.0 / i 159 | * 160 | * def stringify(d: Double): String = d.toString 161 | * 162 | * } 163 | * }}} 164 | * 165 | * Instead, let's make the fact that some of our functions can fail explicit in the return type. 166 | * 167 | * {{{ 168 | * object EitherStyle { 169 | * def parse(s: String): Either[NumberFormatException, Int] = 170 | * if (s.matches("-?[0-9]+")) Either.right(s.toInt) 171 | * else Either.left(new NumberFormatException(s"${s} is not a valid integer.")) 172 | * 173 | * def reciprocal(i: Int): Either[IllegalArgumentException, Double] = 174 | * if (i == 0) Either.left(new IllegalArgumentException("Cannot take reciprocal of 0.")) 175 | * else Either.right(1.0 / i) 176 | * 177 | * def stringify(d: Double): String = d.toString 178 | * 179 | * def magic(s: String): Either[Exception, String] = 180 | * parse(s).flatMap(reciprocal).map(stringify) 181 | * } 182 | * }}} 183 | * 184 | * Do these calls return a `Right` value? 185 | */ 186 | def eitherStyleParse(res0: Boolean, res1: Boolean) = { 187 | EitherStyle.parse("Not a number").isRight should be(res0) 188 | EitherStyle.parse("2").isRight should be(res1) 189 | } 190 | 191 | /** 192 | * Now, using combinators like `flatMap` and `map`, we can compose our functions together. Will 193 | * the following incantations return a `Right` value? 194 | */ 195 | def eitherComposition(res0: Boolean, res1: Boolean, res2: Boolean) = { 196 | import EitherStyle._ 197 | 198 | magic("0").isRight should be(res0) 199 | magic("1").isRight should be(res1) 200 | magic("Not a number").isRight should be(res2) 201 | } 202 | 203 | /** 204 | * With the composite function that we actually care about, we can pass in strings and then 205 | * pattern match on the exception. Because `Either` is a sealed type (often referred to as an 206 | * algebraic data type, or ADT), the compiler will complain if we do not check both the `Left` and 207 | * `Right` case. 208 | * 209 | * In the following exercise we pattern-match on every case the `Either` returned by `magic` can 210 | * be in. If we leave out any of those clauses the compiler will yell at us, as it should. 211 | * However, note the `Left(_)` clause - the compiler will complain if we leave that out because it 212 | * knows that given the type `Either[Exception, String]`, there can be inhabitants of `Left` that 213 | * are not `NumberFormatException` or `IllegalArgumentException`. However, we "know" by inspection 214 | * of the source that those will be the only exceptions thrown, so it seems strange to have to 215 | * account for other exceptions. This implies that there is still room to improve. 216 | */ 217 | def eitherExceptions(res0: String) = { 218 | import EitherStyle._ 219 | 220 | val result = magic("2") match { 221 | case Left(_: NumberFormatException) => "Not a number!" 222 | case Left(_: IllegalArgumentException) => "Can't take reciprocal of 0!" 223 | case Left(_) => "Unknown error" 224 | case Right(result) => s"Got reciprocal: $result" 225 | } 226 | result should be(res0) 227 | } 228 | 229 | /** 230 | * Instead of using exceptions as our error value, let's instead enumerate explicitly the things 231 | * that can go wrong in our program. 232 | * 233 | * {{{ 234 | * object EitherStyleWithAdts { 235 | * sealed abstract class Error 236 | * final case class NotANumber(string: String) extends Error 237 | * final case object NoZeroReciprocal extends Error 238 | * 239 | * def parse(s: String): Either[Error, Int] = 240 | * if (s.matches("-?[0-9]+")) Either.right(s.toInt) 241 | * else Either.left(NotANumber(s)) 242 | * 243 | * def reciprocal(i: Int): Either[Error, Double] = 244 | * if (i == 0) Either.left(NoZeroReciprocal) 245 | * else Either.right(1.0 / i) 246 | * 247 | * def stringify(d: Double): String = d.toString 248 | * 249 | * def magic(s: String): Either[Error, String] = 250 | * parse(s).flatMap(reciprocal).map(stringify) 251 | * } 252 | * }}} 253 | * 254 | * For our little module, we enumerate any and all errors that can occur. Then, instead of using 255 | * exception classes as error values, we use one of the enumerated cases. Now when we pattern 256 | * match, we get much nicer matching. Moreover, since `Error` is `sealed`, no outside code can add 257 | * additional subtypes which we might fail to handle. 258 | */ 259 | def eitherErrorsAsAdts(res0: String) = { 260 | import EitherStyleWithAdts._ 261 | 262 | val result = magic("2") match { 263 | case Left(NotANumber(_)) => "Not a number!" 264 | case Left(NoZeroReciprocal) => "Can't take reciprocal of 0!" 265 | case Right(result) => s"Got reciprocal: $result" 266 | } 267 | result should be(res0) 268 | } 269 | 270 | /** 271 | * =Either in the small, Either in the large= 272 | * 273 | * Once you start using `Either` for all your error-handling, you may quickly run into an issue 274 | * where you need to call into two separate modules which give back separate kinds of errors. 275 | * 276 | * {{{ 277 | * sealed abstract class DatabaseError 278 | * trait DatabaseValue 279 | * 280 | * object Database { 281 | * def databaseThings(): Either[DatabaseError, DatabaseValue] = ??? 282 | * } 283 | * 284 | * sealed abstract class ServiceError 285 | * trait ServiceValue 286 | * 287 | * object Service { 288 | * def serviceThings(v: DatabaseValue): Either[ServiceError, ServiceValue] = ??? 289 | * } 290 | * }}} 291 | * 292 | * Let's say we have an application that wants to do database things, and then take database 293 | * values and do service things. Glancing at the types, it looks like `flatMap` will do it. 294 | * 295 | * {{{ 296 | * def doApp = Database.databaseThings().flatMap(Service.serviceThings) 297 | * }}} 298 | * 299 | * This doesn't work! Well, it does, but it gives us `Either[Object, ServiceValue]` which isn't 300 | * particularly useful for us. Now if we inspect the `Left`s, we have no clue what it could be. 301 | * The reason this occurs is because the first type parameter in the two `Either`s are different - 302 | * `databaseThings()` can give us a `DatabaseError` whereas `serviceThings()` can give us a 303 | * `ServiceError`: two completely unrelated types. Recall that the type parameters of `Either` are 304 | * covariant, so when it sees an `Either[E1, A1]` and an `Either[E2, A2]`, it will happily try to 305 | * unify the `E1` and `E2` in a `flatMap` call - in our case, the closest common supertype is 306 | * `Object`, leaving us with practically no type information to use in our pattern match. 307 | * 308 | * ==Solution 1: Application-wide errors== 309 | * 310 | * So clearly in order for us to easily compose `Either` values, the left type parameter must be 311 | * the same. We may then be tempted to make our entire application share an error data type. 312 | * 313 | * {{{ 314 | * sealed abstract class AppError 315 | * final case object DatabaseError1 extends AppError 316 | * final case object DatabaseError2 extends AppError 317 | * final case object ServiceError1 extends AppError 318 | * final case object ServiceError2 extends AppError 319 | * 320 | * trait DatabaseValue 321 | * 322 | * object Database { 323 | * def databaseThings(): Either[AppError, DatabaseValue] = ??? 324 | * } 325 | * 326 | * object Service { 327 | * def serviceThings(v: DatabaseValue): Either[AppError, ServiceValue] = ??? 328 | * } 329 | * 330 | * def doApp = Database.databaseThings().flatMap(Service.serviceThings) 331 | * }}} 332 | * 333 | * This certainly works, or at least it compiles. But consider the case where another module wants 334 | * to just use `Database`, and gets an `Either[AppError, DatabaseValue]` back. Should it want to 335 | * inspect the errors, it must inspect **all** the `AppError` cases, even though it was only 336 | * intended for `Database` to use `DatabaseError1` or `DatabaseError2`. 337 | * 338 | * ==Solution 2: ADTs all the way down== 339 | * 340 | * Instead of lumping all our errors into one big ADT, we can instead keep them local to each 341 | * module, and have an application-wide error ADT that wraps each error ADT we need. 342 | * 343 | * {{{ 344 | * sealed abstract class DatabaseError 345 | * trait DatabaseValue 346 | * 347 | * object Database { 348 | * def databaseThings(): Either[DatabaseError, DatabaseValue] = ??? 349 | * } 350 | * 351 | * sealed abstract class ServiceError 352 | * trait ServiceValue 353 | * 354 | * object Service { 355 | * def serviceThings(v: DatabaseValue): Either[ServiceError, ServiceValue] = ??? 356 | * } 357 | * 358 | * sealed abstract class AppError 359 | * object AppError { 360 | * final case class Database(error: DatabaseError) extends AppError 361 | * final case class Service(error: ServiceError) extends AppError 362 | * } 363 | * }}} 364 | * 365 | * Now in our outer application, we can wrap/lift each module-specific error into `AppError` and 366 | * then call our combinators as usual. `Either` provides a convenient method to assist with this, 367 | * called `Either.leftMap` - it can be thought of as the same as `map`, but for the `Left` side. 368 | * 369 | * {{{ 370 | * def doApp: Either[AppError, ServiceValue] = 371 | * Database.databaseThings().leftMap(AppError.Database). 372 | * flatMap(dv => Service.serviceThings(dv).leftMap(AppError.Service)) 373 | * }}} 374 | * 375 | * Hurrah! Each module only cares about its own errors as it should be, and more composite modules 376 | * have their own error ADT that encapsulates each constituent module's error ADT. Doing this also 377 | * allows us to take action on entire classes of errors instead of having to pattern match on each 378 | * individual one. 379 | * 380 | * {{{ 381 | * def awesome = 382 | * doApp match { 383 | * case Left(AppError.Database(_)) => "something in the database went wrong" 384 | * case Left(AppError.Service(_)) => "something in the service went wrong" 385 | * case Right(_) => "everything is alright!" 386 | * } 387 | * }}} 388 | * 389 | * Let's review the `leftMap` and `map` methods: 390 | */ 391 | def eitherInTheLarge( 392 | res0: Either[String, Int], 393 | res1: Either[String, Int], 394 | res2: Either[String, Int] 395 | ) = { 396 | val right: Either[String, Int] = Right(41) 397 | right.map(_ + 1) should be(res0) 398 | 399 | val left: Either[String, Int] = Left("Hello") 400 | left.map(_ + 1) should be(res1) 401 | left.leftMap(_.reverse) should be(res2) 402 | } 403 | 404 | /** 405 | * There will inevitably come a time when your nice `Either` code will have to interact with 406 | * exception-throwing code. Handling such situations is easy enough. 407 | * 408 | * {{{ 409 | * val either: Either[NumberFormatException, Int] = 410 | * try { 411 | * Either.right("abc".toInt) 412 | * } catch { 413 | * case nfe: NumberFormatException => Either.left(nfe) 414 | * } 415 | * }}} 416 | * 417 | * However, this can get tedious quickly. `Either` provides a `catchOnly` method on its companion 418 | * object that allows you to pass it a function, along with the type of exception you want to 419 | * catch, and does the above for you. 420 | * 421 | * {{{ 422 | * val either: Either[NumberFormatException, Int] = 423 | * Either.catchOnly[NumberFormatException]("abc".toInt) 424 | * }}} 425 | * 426 | * If you want to catch all (non-fatal) throwables, you can use `catchNonFatal`. 427 | */ 428 | def eitherWithExceptions(res0: Boolean, res1: Boolean) = { 429 | Either.catchOnly[NumberFormatException]("abc".toInt).isRight should be(res0) 430 | 431 | Either.catchNonFatal(1 / 0).isLeft should be(res1) 432 | } 433 | 434 | /** 435 | * =Additional syntax= 436 | * 437 | * For using Either's syntax on arbitrary data types, you can import `cats.implicits._`. This will 438 | * make possible to use the `asLeft` and `asRight` methods: 439 | * 440 | * {{{ 441 | * import cats.implicits._ 442 | * 443 | * val right: Either[String, Int] = 7.asRight[String] 444 | * 445 | * val left: Either[String, Int] = "hello 🐈s".asLeft[Int] 446 | * }}} 447 | * 448 | * These method promote values to the `Either` data type: 449 | */ 450 | def eitherSyntax(res0: Either[String, Int]) = { 451 | import cats.implicits._ 452 | 453 | val right: Either[String, Int] = 42.asRight[String] 454 | right should be(res0) 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/main/scala/catslib/EvalSection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats._ 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import org.scalatest.matchers.should.Matchers 22 | 23 | /** 24 | * Eval is a data type for controlling synchronous evaluation. Its implementation is designed to 25 | * provide stack-safety at all times using a technique called trampolining. There are two different 26 | * factors that play into evaluation: memoization and laziness. Memoized evaluation evaluates an 27 | * expression only once and then remembers (memoizes) that value. Lazy evaluation refers to when the 28 | * expression is evaluated. We talk about eager evaluation if the expression is immediately 29 | * evaluated when defined and about lazy evaluation if the expression is evaluated when it’s first 30 | * used. For example, in Scala, a lazy val is both lazy and memoized, a method definition def is 31 | * lazy, but not memoized, since the body will be evaluated on every call. A normal val evaluates 32 | * eagerly and also memoizes the result. Eval is able to express all of these evaluation strategies 33 | * and allows us to chain computations using its Monad instance. 34 | * 35 | * @param name 36 | * Eval 37 | */ 38 | object EvalSection extends AnyFlatSpec with Matchers with org.scalaexercises.definitions.Section { 39 | 40 | /** 41 | * =Eval.now= 42 | * 43 | * First of the strategies is eager evaluation, we can construct an Eval eagerly using Eval.now: 44 | * 45 | * {{{ 46 | * import cats.Eval 47 | * // import cats.Eval 48 | * 49 | * import cats.implicits._ 50 | * // import cats.implicits._ 51 | * 52 | * val eager = Eval.now { 53 | * println("Running expensive calculation...") 54 | * 1 + 2 * 3 55 | * } 56 | * // Running expensive calculation... 57 | * // eager: cats.Eval[Int] = Now(7) 58 | * }}} 59 | * 60 | * We can run the computation using the given evaluation strategy anytime by using the value 61 | * method. eager.value // res0: Int = 7 62 | */ 63 | def nowEval(res0: List[Int]) = { 64 | // given 65 | val eagerEval = Eval.now { 66 | println("This is eagerly evaluated") 67 | 1 :: 2 :: 3 :: Nil 68 | } 69 | 70 | // when/then 71 | eagerEval.value shouldBe (res0) 72 | } 73 | 74 | /** 75 | * =Eval.later= 76 | * 77 | * If we want lazy evaluation, we can use Eval.later In this case 78 | * 79 | * {{{ 80 | * val lazyEval = Eval.later { 81 | * println("Running expensive calculation...") 82 | * 1 + 2 * 3 83 | * } 84 | * // lazyEval: cats.Eval[Int] = cats.Later@6c2b03e9 85 | * 86 | * lazyEval.value 87 | * // Running expensive calculation... 88 | * // res1: Int = 7 89 | * 90 | * lazyEval.value 91 | * // res2: Int = 7 92 | * }}} 93 | * 94 | * Notice that “Running expensive calculation” is printed only once, since the value was memoized 95 | * internally. Meaning also that the resulted operation was only computed once. Eval.later is 96 | * different to using a lazy val in a few different ways. First, it allows the runtime to perform 97 | * garbage collection of the thunk after evaluation, leading to more memory being freed earlier. 98 | * Secondly, when lazy vals are evaluated, in order to preserve thread-safety, the Scala compiler 99 | * will lock the whole surrounding class, whereas Eval will only lock itself. 100 | */ 101 | def laterEval(res0: List[Int], res1: Int) = { 102 | // given 103 | val n = 2 104 | var counter = 0 105 | val lazyEval = Eval.later { 106 | println("This is lazyly evaluated with caching") 107 | counter = counter + 1 108 | (1 to n) 109 | } 110 | 111 | // when/then 112 | List.fill(n)("").foreach(_ => lazyEval.value) 113 | lazyEval.value shouldBe res0 114 | counter shouldBe res1 115 | } 116 | 117 | /** 118 | * =Eval.always= 119 | * 120 | * If we want lazy evaluation, but without memoization akin to Function0, we can use Eval.always 121 | * Here we can see, that the expression is evaluated every time we call .value. 122 | * {{{ 123 | * val alwaysEval = Eval.always(println("Always evaluated")) 124 | * //Always evaluated 125 | * alwaysEval.eval 126 | * //Always evaluated 127 | * alwaysEval.eval 128 | * //Always evaluated 129 | * alwaysEval.eval 130 | * }}} 131 | */ 132 | def alwaysEval(res0: Int, res1: List[Int], res2: Int) = { 133 | // given 134 | val n = 4 135 | var counter = 0 136 | val alwaysEval = Eval.always { 137 | println("This is lazyly evaluated without caching") 138 | counter = counter + 1 139 | (1 to n) 140 | } 141 | 142 | // when/then 143 | List.fill(n)("").foreach(_ => alwaysEval.value) 144 | counter shouldBe res0 145 | alwaysEval.value shouldBe res1 146 | counter shouldBe res2 147 | } 148 | 149 | /** 150 | * =Eval.defer= 151 | * 152 | * Defer a computation which produces an Eval[A] value This is useful when you want to delay 153 | * execution of an expression which produces an Eval[A] value. Like .flatMap, it is stack-safe. 154 | * Because Eval guarantees stack-safety, we can chain a lot of computations together using flatMap 155 | * without fear of blowing up the stack. 156 | */ 157 | def deferEval(res0: List[Int]) = { 158 | // given 159 | val list = List.fill(3)(0) 160 | 161 | // when 162 | val deferedEval: Eval[List[Int]] = Eval.now(list).flatMap(e => Eval.defer(Eval.later(e))) 163 | 164 | // then 165 | Eval.defer(deferedEval).value shouldBe res0 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Foldable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats._ 23 | import cats.implicits._ 24 | 25 | /** 26 | * Foldable type class instances can be defined for data structures that can be folded to a summary 27 | * value. 28 | * 29 | * In the case of a collection (such as `List` or `Set`), these methods will fold together (combine) 30 | * the values contained in the collection to produce a single result. Most collection types have 31 | * `foldLeft` methods, which will usually be used by the associated `Foldable[_]` instance. 32 | * 33 | * `Foldable[F]` is implemented in terms of two basic methods: 34 | * 35 | * - `foldLeft(fa, b)(f)` eagerly folds `fa` from left-to-right. 36 | * - `foldRight(fa, b)(f)` lazily folds `fa` from right-to-left. 37 | * 38 | * These form the basis for many other operations, see also: 39 | * [[http://www.cs.nott.ac.uk/~pszgmh/fold.pdf A tutorial on the universality and expressiveness of fold]] 40 | * 41 | * First some standard imports. 42 | * 43 | * {{{ 44 | * import cats._ 45 | * import cats.implicits._ 46 | * }}} 47 | * 48 | * Apart from the familiar `foldLeft` and `foldRight`, `Foldable` has a number of other useful 49 | * functions. 50 | * 51 | * @param name 52 | * foldable 53 | */ 54 | object FoldableSection 55 | extends AnyFlatSpec 56 | with Matchers 57 | with org.scalaexercises.definitions.Section { 58 | 59 | /** 60 | * =foldLeft= 61 | * 62 | * `foldLeft` is an eager left-associative fold on `F` using the given function. 63 | */ 64 | def foldableFoldLeft(res0: Int, res1: String) = { 65 | Foldable[List].foldLeft(List(1, 2, 3), 0)(_ + _) should be(res0) 66 | Foldable[List].foldLeft(List("a", "b", "c"), "")(_ + _) should be(res1) 67 | } 68 | 69 | /** 70 | * =foldRight= 71 | * 72 | * `foldRight` is a lazy right-associative fold on `F` using the given function. The function has 73 | * the signature `(A, Eval[B]) => Eval[B]` to support laziness in a stack-safe way. 74 | */ 75 | def foldableFoldRight(res0: Int) = { 76 | val lazyResult = 77 | Foldable[List].foldRight(List(1, 2, 3), Now(0))((x, rest) => Later(x + rest.value)) 78 | lazyResult.value should be(res0) 79 | } 80 | 81 | /** 82 | * =fold= 83 | * 84 | * `fold`, also called `combineAll`, combines every value in the foldable using the given `Monoid` 85 | * instance. 86 | */ 87 | def foldableFold(res0: String, res1: Int) = { 88 | Foldable[List].fold(List("a", "b", "c")) should be(res0) 89 | Foldable[List].fold(List(1, 2, 3)) should be( 90 | res1 91 | ) // Hint: the implicit monoid for `Int` is the Sum monoid 92 | } 93 | 94 | /** 95 | * =foldMap= 96 | * 97 | * `foldMap` is similar to `fold` but maps every `A` value into `B` and then combines them using 98 | * the given `Monoid[B]` instance. 99 | */ 100 | def foldableFoldMap(res0: Int, res1: String) = { 101 | Foldable[List].foldMap(List("a", "b", "c"))(_.length) should be(res0) 102 | Foldable[List].foldMap(List(1, 2, 3))(_.toString) should be(res1) 103 | } 104 | 105 | /** 106 | * =foldK= 107 | * 108 | * `foldK` is similar to `fold` but combines every value in the foldable using the given 109 | * `MonoidK[G]` instance instead of `Monoid[G]`. 110 | */ 111 | def foldableFoldk(res0: List[Int], res1: Option[String]) = { 112 | Foldable[List].foldK(List(List(1, 2), List(3, 4, 5))) should be(res0) 113 | Foldable[List].foldK(List(None, Option("two"), Option("three"))) should be(res1) 114 | } 115 | 116 | /** 117 | * =find= 118 | * 119 | * `find` searches for the first element matching the predicate, if one exists. 120 | */ 121 | def foldableFind(res0: Option[Int], res1: Option[Int]) = { 122 | Foldable[List].find(List(1, 2, 3))(_ > 2) should be(res0) 123 | Foldable[List].find(List(1, 2, 3))(_ > 5) should be(res1) 124 | } 125 | 126 | /** 127 | * =exists= 128 | * 129 | * `exists` checks whether at least one element satisfies the predicate. 130 | */ 131 | def foldableExists(res0: Boolean, res1: Boolean) = { 132 | Foldable[List].exists(List(1, 2, 3))(_ > 2) should be(res0) 133 | Foldable[List].exists(List(1, 2, 3))(_ > 5) should be(res1) 134 | } 135 | 136 | /** 137 | * =forall= 138 | * 139 | * `forall` checks whether all elements satisfy the predicate. 140 | */ 141 | def foldableForall(res0: Boolean, res1: Boolean) = { 142 | Foldable[List].forall(List(1, 2, 3))(_ <= 3) should be(res0) 143 | Foldable[List].forall(List(1, 2, 3))(_ < 3) should be(res1) 144 | } 145 | 146 | /** 147 | * =toList= 148 | * 149 | * Convert `F[A]` to `List[A]`. 150 | */ 151 | def foldableTolist(res0: List[Int], res1: List[Int], res2: List[Int]) = { 152 | Foldable[List].toList(List(1, 2, 3)) should be(res0) 153 | Foldable[Option].toList(Option(42)) should be(res1) 154 | Foldable[Option].toList(None) should be(res2) 155 | } 156 | 157 | /** 158 | * =filter_= 159 | * 160 | * Convert `F[A]` to `List[A]` only including the elements that match a predicate. 161 | */ 162 | def foldableFilter(res0: List[Int], res1: List[Int]) = { 163 | Foldable[List].filter_(List(1, 2, 3))(_ < 3) should be(res0) 164 | Foldable[Option].filter_(Option(42))(_ != 42) should be(res1) 165 | } 166 | 167 | /** 168 | * =traverse_= 169 | * 170 | * `traverse` the foldable mapping `A` values to `G[B]`, and combining them using `Applicative[G]` 171 | * and discarding the results. 172 | * 173 | * This method is primarily useful when `G[_]` represents an action or effect, and the specific 174 | * `B` aspect of `G[B]` is not otherwise needed. The `B` will be discarded and `Unit` returned 175 | * instead. 176 | */ 177 | def foldableTraverse(res0: Option[Unit], res1: Option[Unit]) = { 178 | import cats.implicits._ 179 | 180 | def parseInt(s: String): Option[Int] = 181 | Either.catchOnly[NumberFormatException](s.toInt).toOption 182 | 183 | Foldable[List].traverse_(List("1", "2", "3"))(parseInt) should be(res0) 184 | Foldable[List].traverse_(List("a", "b", "c"))(parseInt) should be(res1) 185 | } 186 | 187 | /** 188 | * =compose= 189 | * 190 | * We can compose `Foldable[F[_]]` and `Foldable[G[_]]` instances to obtain `Foldable[F[G]]`. 191 | */ 192 | def foldableCompose(res0: Int, res1: String) = { 193 | val FoldableListOption = Foldable[List].compose[Option] 194 | FoldableListOption.fold(List(Option(1), Option(2), Option(3), Option(4))) should be(res0) 195 | FoldableListOption.fold(List(Option("1"), Option("2"), None, Option("3"))) should be(res1) 196 | } 197 | 198 | /** 199 | * =More Foldable methods= 200 | * 201 | * Hence when defining some new data structure, if we can define a `foldLeft` and `foldRight` we 202 | * are able to provide many other useful operations, if not always the most efficient 203 | * implementations, over the structure without further implementation. 204 | * 205 | * There are a few more methods that we haven't talked about but you probably can guess what they 206 | * do: 207 | */ 208 | def foldableMethods(res0: Boolean, res1: List[Int], res2: List[Int]) = { 209 | Foldable[List].isEmpty(List(1, 2, 3)) should be(res0) 210 | Foldable[List].dropWhile_(List(1, 2, 3))(_ < 2) should be(res1) 211 | Foldable[List].takeWhile_(List(1, 2, 3))(_ < 2) should be(res2) 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /src/main/scala/catslib/FunctorSection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats._ 23 | import cats.implicits._ 24 | 25 | /** 26 | * A `Functor` is a ubiquitous type class involving types that have one "hole", i.e. types which 27 | * have the shape `F[*]`, such as `Option`, `List` and `Future`. (This is in contrast to a type like 28 | * `Int` which has no hole, or `Tuple2` which has two holes (`Tuple2[*,*]`)). 29 | * 30 | * The `Functor` category involves a single operation, named `map`: 31 | * 32 | * {{{ 33 | * def map[A, B](fa: F[A])(f: A => B): F[B] 34 | * }}} 35 | * 36 | * This method takes a function `A => B` and turns an `F[A]` into an `F[B]`. The name of the method 37 | * `map` should remind you of the `map` method that exists on many classes in the Scala standard 38 | * library, for example: 39 | * 40 | * {{{ 41 | * Option(1).map(_ + 1) 42 | * List(1,2,3).map(_ + 1) 43 | * Vector(1,2,3).map(_.toString) 44 | * }}} 45 | * 46 | * =Creating Functor instances= 47 | * 48 | * We can trivially create a `Functor` instance for a type which has a well behaved `map` method: 49 | * 50 | * {{{ 51 | * import cats._ 52 | * 53 | * implicit val optionFunctor: Functor[Option] = new Functor[Option] { 54 | * def map[A,B](fa: Option[A])(f: A => B) = fa map f 55 | * } 56 | * 57 | * implicit val listFunctor: Functor[List] = new Functor[List] { 58 | * def map[A,B](fa: List[A])(f: A => B) = fa map f 59 | * } 60 | * }}} 61 | * 62 | * However, functors can also be created for types which don't have a `map` method. For example, if 63 | * we create a `Functor` for `Function1[In, *]` we can use `andThen` to implement `map`: 64 | * 65 | * {{{ 66 | * implicit def function1Functor[In]: Functor[Function1[In, *]] = 67 | * new Functor[Function1[In, *]] { 68 | * def map[A,B](fa: In => A)(f: A => B): Function1[In,B] = fa andThen f 69 | * } 70 | * }}} 71 | * 72 | * This example demonstrates the use of the 73 | * [[https://github.com/non/kind-projector kind-projector compiler plugin]] This compiler plugin can 74 | * help us when we need to change the number of type holes. In the example above, we took a type 75 | * which normally has two type holes, `Function1[*,*]` and constrained one of the holes to be the 76 | * `In` type, leaving just one hole for the return type, resulting in `Function1[In,*]`. Without 77 | * kind-projector, we'd have to write this as something like `({type F[A] = Function1[In,A]})#F`, 78 | * which is much harder to read and understand. 79 | * 80 | * @param name 81 | * functor 82 | */ 83 | object FunctorSection 84 | extends AnyFlatSpec 85 | with Matchers 86 | with org.scalaexercises.definitions.Section { 87 | 88 | /** 89 | * =Using Functor= 90 | * 91 | * ==map== 92 | * 93 | * `List` is a functor which applies the function to each element of the list: 94 | * 95 | * {{{ 96 | * Functor[List].map(List("qwer", "adsfg"))(_.length) 97 | * }}} 98 | * 99 | * `Option` is a functor which only applies the function when the `Option` value is a `Some`: 100 | */ 101 | def usingFunctor(res0: Option[Int], res1: Option[Int]) = { 102 | Functor[Option].map(Option("Hello"))(_.length) should be(res0) 103 | Functor[Option].map(None: Option[String])(_.length) should be(res1) 104 | } 105 | 106 | /** 107 | * =Derived methods= 108 | * 109 | * ==lift== 110 | * 111 | * We can use `Functor` to "lift" a function from `A => B` to `F[A] => F[B]`: 112 | * 113 | * {{{ 114 | * val lenOption: Option[String] => Option[Int] = Functor[Option].lift(_.length) 115 | * lenOption(Some("abcd")) 116 | * }}} 117 | * 118 | * We can now apply the `lenOption` function to `Option` instances. 119 | */ 120 | def liftingToAFunctor(res0: Option[Int]) = { 121 | val lenOption: Option[String] => Option[Int] = Functor[Option].lift(_.length) 122 | lenOption(Some("Hello")) should be(res0) 123 | } 124 | 125 | /** 126 | * ==fproduct== 127 | * 128 | * `Functor` provides an `fproduct` function which pairs a value with the result of applying a 129 | * function to that value. 130 | */ 131 | def usingFproduct(res0: Int, res1: Int, res2: Int) = { 132 | val source = List("Cats", "is", "awesome") 133 | val product = Functor[List].fproduct(source)(_.length).toMap 134 | 135 | product.get("Cats").getOrElse(0) should be(res0) 136 | product.get("is").getOrElse(0) should be(res1) 137 | product.get("awesome").getOrElse(0) should be(res2) 138 | } 139 | 140 | /** 141 | * ==compose== 142 | * 143 | * Functors compose! Given any functor `F[_]` and any functor `G[_]` we can create a new functor 144 | * `F[G[_]]` by composing them: 145 | * 146 | * {{{ 147 | * val listOpt = Functor[List] compose Functor[Option] 148 | * }}} 149 | * 150 | * In the previous example the resulting functor will apply the `map` operation through the two 151 | * type constructors: `List` and `Option`. 152 | */ 153 | def composingFunctors(res0: List[Option[Int]]) = { 154 | val listOpt = Functor[List] compose Functor[Option] 155 | listOpt.map(List(Some(1), None, Some(3)))(_ + 1) should be(res0) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/scala/catslib/IdentitySection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats._ 20 | import org.scalatest.matchers.should.Matchers 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | 23 | /** 24 | * The identity monad can be seen as the ambient monad that encodes the effect of having no effect. 25 | * It is ambient in the sense that plain pure values are values of `Id`. 26 | * 27 | * It is encoded as: 28 | * 29 | * {{{ 30 | * type Id[A] = A 31 | * }}} 32 | * 33 | * That is to say that the type Id[A] is just a synonym for A. We can freely treat values of type 34 | * `A` as values of type `Id[A]`, and vice-versa. 35 | * 36 | * {{{ 37 | * import cats._ 38 | * 39 | * val x: Id[Int] = 1 40 | * val y: Int = x 41 | * }}} 42 | * 43 | * @param name 44 | * identity 45 | */ 46 | object IdentitySection 47 | extends AnyFlatSpec 48 | with Matchers 49 | with org.scalaexercises.definitions.Section { 50 | 51 | /** 52 | * We can freely compare values of `Id[T]` with unadorned values of type `T`. 53 | */ 54 | def identityType(res0: Int) = { 55 | val anId: Id[Int] = 42 56 | anId should be(res0) 57 | } 58 | 59 | /** 60 | * Using this type declaration, we can treat our Id type constructor as a `Monad` and as a 61 | * `Comonad`. The `pure` method, which has type `A => Id[A]` just becomes the identity function. 62 | * The `map` method from `Functor` just becomes function application: 63 | * 64 | * {{{ 65 | * import cats.Functor 66 | * 67 | * val one: Int = 1 68 | * Functor[Id].map(one)(_ + 1) 69 | * }}} 70 | */ 71 | def pureIdentity(res0: Int) = 72 | Applicative[Id].pure(42) should be(res0) 73 | 74 | /** 75 | * Compare the signatures of `map` and `flatMap` and `coflatMap`: 76 | * 77 | * {{{ 78 | * def map[A, B](fa: Id[A])(f: A => B): Id[B] 79 | * def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] 80 | * def coflatMap[A, B](a: Id[A])(f: Id[A] => B): Id[B] 81 | * }}} 82 | * 83 | * You'll notice that in the flatMap signature, since `Id[B]` is the same as `B` for all B, we can 84 | * rewrite the type of the `f` parameter to be `A => B` instead of `A => Id[B]`, and this makes 85 | * the signatures of the two functions the same, and, in fact, they can have the same 86 | * implementation, meaning that for `Id`, `flatMap` is also just function application: 87 | * 88 | * {{{ 89 | * import cats.Monad 90 | * 91 | * val one: Int = 1 92 | * Monad[Id].map(one)(_ + 1) 93 | * Monad[Id].flatMap(one)(_ + 1) 94 | * }}} 95 | */ 96 | def idComonad(res0: Int) = { 97 | val fortytwo: Int = 42 98 | Comonad[Id].coflatMap(fortytwo)(_ + 1) should be(res0) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Monad.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import MonadHelpers._ 23 | 24 | /** 25 | * `Monad` extends the `Applicative` type class with a new function `flatten`. Flatten takes a value 26 | * in a nested context (eg. `F[F[A]]` where F is the context) and "joins" the contexts together so 27 | * that we have a single context (ie. `F[A]`). 28 | * 29 | * @param name 30 | * monad 31 | */ 32 | object MonadSection extends AnyFlatSpec with Matchers with org.scalaexercises.definitions.Section { 33 | 34 | /** 35 | * The name `flatten` should remind you of the functions of the same name on many classes in the 36 | * standard library. 37 | */ 38 | def flattenRecap(res0: Option[Int], res1: Option[Int], res2: List[Int]) = { 39 | Option(Option(1)).flatten should be(res0) 40 | Option(None).flatten should be(res1) 41 | List(List(1), List(2, 3)).flatten should be(res2) 42 | } 43 | 44 | /** 45 | * =Monad instances= 46 | * 47 | * If `Applicative` is already present and `flatten` is well-behaved, extending the `Applicative` 48 | * to a `Monad` is trivial. To provide evidence that a type belongs in the `Monad` type class, 49 | * cats' implementation requires us to provide an implementation of `pure` (which can be reused 50 | * from `Applicative`) and `flatMap`. 51 | * 52 | * We can use `flatten` to define `flatMap`: `flatMap` is just `map` followed by `flatten`. 53 | * Conversely, `flatten` is just `flatMap` using the identity function `x => x` (i.e. 54 | * `flatMap(_)(x => x)`). 55 | * 56 | * {{{ 57 | * import cats._ 58 | * 59 | * implicit def optionMonad(implicit app: Applicative[Option]) = 60 | * new Monad[Option] { 61 | * // Define flatMap using Option's flatten method 62 | * override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = 63 | * app.map(fa)(f).flatten 64 | * // Reuse this definition from Applicative. 65 | * override def pure[A](a: A): Option[A] = app.pure(a) 66 | * } 67 | * }}} 68 | * 69 | * Cats already provides a `Monad` instance of `Option`. 70 | */ 71 | def monadInstances(res0: Option[Int]) = { 72 | import cats._ 73 | import cats.implicits._ 74 | 75 | Monad[Option].pure(42) should be(res0) 76 | } 77 | 78 | /** 79 | * =flatMap= 80 | * 81 | * `flatMap` is often considered to be the core function of `Monad`, and cats follows this 82 | * tradition by providing implementations of `flatten` and `map` derived from `flatMap` and 83 | * `pure`. 84 | * 85 | * {{{ 86 | * implicit val listMonad = new Monad[List] { 87 | * def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f) 88 | * def pure[A](a: A): List[A] = List(a) 89 | * } 90 | * }}} 91 | * 92 | * Part of the reason for this is that name `flatMap` has special significance in scala, as 93 | * for-comprehensions rely on this method to chain together operations in a monadic context. 94 | */ 95 | def monadFlatmap(res0: List[Int]) = { 96 | import cats._ 97 | import cats.implicits._ 98 | 99 | Monad[List].flatMap(List(1, 2, 3))(x => List(x, x)) should be(res0) 100 | } 101 | 102 | /** 103 | * =ifM= 104 | * 105 | * `Monad` provides the ability to choose later operations in a sequence based on the results of 106 | * earlier ones. This is embodied in `ifM`, which lifts an `if` statement into the monadic 107 | * context. 108 | */ 109 | def monadIfm(res0: Option[String], res1: List[Int], res2: List[Int]) = { 110 | import cats._ 111 | import cats.implicits._ 112 | 113 | Monad[Option].ifM(Option(true))(Option("truthy"), Option("falsy")) should be(res0) 114 | Monad[List].ifM(List(true, false, true))(List(1, 2), List(3, 4)) should be(res1) 115 | Monad[List].ifM(List(false, true, false))(List(1, 2), List(3, 4)) should be(res2) 116 | } 117 | 118 | /** 119 | * =Composition= 120 | * 121 | * Unlike `Functor`s and `Applicative`s, you cannot derive a monad instance for a generic 122 | * `M[N[_]]` where both `M[_]` and `N[_]` have an instance of a monad. 123 | * 124 | * However, it is common to want to compose the effects of both `M[_]` and `N[_]`. One way of 125 | * expressing this is to provide instructions on how to compose any outer monad (`F` in the 126 | * following example) with a specific inner monad (`Option` in the following example). 127 | * 128 | * {{{ 129 | * case class OptionT[F[_], A](value: F[Option[A]]) 130 | * 131 | * implicit def optionTMonad[F[_]](implicit F : Monad[F]) = { 132 | * new Monad[OptionT[F, *]] { 133 | * def pure[A](a: A): OptionT[F, A] = OptionT(F.pure(Some(a))) 134 | * def flatMap[A, B](fa: OptionT[F, A])(f: A => OptionT[F, B]): OptionT[F, B] = 135 | * OptionT { 136 | * F.flatMap(fa.value) { 137 | * case None => F.pure(None) 138 | * case Some(a) => f(a).value 139 | * } 140 | * } 141 | * def tailRecM[A, B](a: A)(f: A => OptionT[F, Either[A, B]]): OptionT[F, B] = 142 | * defaultTailRecM(a)(f) 143 | * } 144 | * } 145 | * }}} 146 | * 147 | * This sort of construction is called a monad transformer. Cats already provides a monad 148 | * transformer for `Option` called `OptionT`. 149 | */ 150 | def monadComposition(res0: List[Option[Int]]) = { 151 | import cats.implicits._ 152 | 153 | optionTMonad[List].pure(42) should be(OptionT(res0)) 154 | } 155 | 156 | /** 157 | * There are also instances for other monads available for user in Cats library: 'EitherT' for 158 | * 'Either' 'ReaderT' for 'Reader' 'WriterT' for 'Writer' 'StateT' for 'State' 'IdT' for 'Id' 159 | */ 160 | } 161 | -------------------------------------------------------------------------------- /src/main/scala/catslib/MonadHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats.implicits._ 20 | import cats.Monad 21 | 22 | object MonadHelpers { 23 | case class OptionT[F[_], A](value: F[Option[A]]) 24 | 25 | implicit def optionTMonad[F[_]](implicit F: Monad[F]) = { 26 | new Monad[OptionT[F, *]] { 27 | def pure[A](a: A): OptionT[F, A] = OptionT(F.pure(Some(a))) 28 | def flatMap[A, B](fa: OptionT[F, A])(f: A => OptionT[F, B]): OptionT[F, B] = 29 | OptionT { 30 | F.flatMap(fa.value) { 31 | case None => F.pure(None) 32 | case Some(a) => f(a).value 33 | } 34 | } 35 | 36 | def tailRecM[A, B](a: A)(f: A => OptionT[F, Either[A, B]]): OptionT[F, B] = 37 | OptionT( 38 | F.tailRecM(a)(a0 => 39 | F.map(f(a0).value)( 40 | _.fold(Either.right[A, Option[B]](None))(_.map(b => Some(b): Option[B])) 41 | ) 42 | ) 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Monoid.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats._ 23 | import cats.implicits._ 24 | 25 | /** 26 | * `Monoid` extends the `Semigroup` type class, adding an `empty` method to semigroup's `combine`. 27 | * The `empty` method must return a value that when combined with any other instance of that type 28 | * returns the other instance, i.e. 29 | * 30 | * {{{ 31 | * (combine(x, empty) == combine(empty, x) == x) 32 | * }}} 33 | * 34 | * For example, if we have a `Monoid[String]` with `combine` defined as string concatenation, then 35 | * `empty = ""`. 36 | * 37 | * Having an `empty` defined allows us to combine all the elements of some potentially empty 38 | * collection of `T` for which a `Monoid[T]` is defined and return a `T`, rather than an `Option[T]` 39 | * as we have a sensible default to fall back to. 40 | * 41 | * @param name 42 | * monoid 43 | */ 44 | object MonoidSection extends AnyFlatSpec with Matchers with org.scalaexercises.definitions.Section { 45 | 46 | /** 47 | * First some imports. 48 | * 49 | * {{{ 50 | * import cats._ 51 | * import cats.implicits._ 52 | * }}} 53 | * 54 | * And let's see the implicit instance of `Monoid[String]` in action. 55 | */ 56 | def monoidEmpty(res0: String, res1: String, res2: String) = { 57 | import cats.implicits._ 58 | 59 | Monoid[String].empty should be(res0) 60 | Monoid[String].combineAll(List("a", "b", "c")) should be(res1) 61 | Monoid[String].combineAll(List()) should be(res2) 62 | } 63 | 64 | /** 65 | * The advantage of using these type class provided methods, rather than the specific ones for 66 | * each type, is that we can compose monoids to allow us to operate on more complex types, e.g. 67 | */ 68 | def monoidAdvantage(res0: Map[String, Int], res1: Map[String, Int]) = { 69 | Monoid[Map[String, Int]].combineAll(List(Map("a" -> 1, "b" -> 2), Map("a" -> 3))) should be( 70 | res0 71 | ) 72 | Monoid[Map[String, Int]].combineAll(List()) should be(res1) 73 | } 74 | 75 | /** 76 | * This is also true if we define our own instances. As an example, let's use `Foldable`'s 77 | * `foldMap`, which maps over values accumulating the results, using the available `Monoid` for 78 | * the type mapped onto. 79 | */ 80 | def monoidFoldmap(res0: Int, res1: String) = { 81 | val l = List(1, 2, 3, 4, 5) 82 | l.foldMap(identity) should be(res0) 83 | l.foldMap(i => i.toString) should be(res1) 84 | } 85 | 86 | /** 87 | * To use this with a function that produces a tuple, we can define a `Monoid` for a tuple that 88 | * will be valid for any tuple where the types it contains also have a `Monoid` available. Note 89 | * that cats already defines it for you. 90 | * 91 | * {{{ 92 | * implicit def monoidTuple[A: Monoid, B: Monoid]: Monoid[(A, B)] = 93 | * new Monoid[(A, B)] { 94 | * def combine(x: (A, B), y: (A, B)): (A, B) = { 95 | * val (xa, xb) = x 96 | * val (ya, yb) = y 97 | * (Monoid[A].combine(xa, ya), Monoid[B].combine(xb, yb)) 98 | * } 99 | * def empty: (A, B) = (Monoid[A].empty, Monoid[B].empty) 100 | * } 101 | * }}} 102 | * 103 | * This way we are able to combine both values in one pass, hurrah! 104 | */ 105 | def tupleMonoid(res0: (Int, String)) = { 106 | val l = List(1, 2, 3, 4, 5) 107 | l.foldMap(i => (i, i.toString)) should be(res0) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/scala/catslib/OptionTSection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats._ 20 | import cats.data.OptionT 21 | import cats.implicits._ 22 | import org.scalatest.concurrent.ScalaFutures 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | 26 | import scala.concurrent.ExecutionContext.Implicits.global 27 | import scala.concurrent.Future 28 | 29 | /** 30 | * OptionT is a Monad Transformer that has two type parameters F and A. F is the wrapping Monad and 31 | * A is type inside Option. As a result, OptionT[F[_], A] is a light wrapper on an F[Option[A]]. As 32 | * OptionT is also a monad, it can be used in a for-comprehension and be more convenient to work 33 | * with than using F[Option[A]] directly. 34 | * 35 | * @param name 36 | * OptionT 37 | */ 38 | object OptionTSection 39 | extends AnyFlatSpec 40 | with Matchers 41 | with ScalaFutures 42 | with org.scalaexercises.definitions.Section { 43 | 44 | implicit val futureOptionEq = new Eq[Future[Option[String]]] { 45 | def eqv(x: Future[Option[String]], y: Future[Option[String]]): Boolean = 46 | x.futureValue.eqv(y.futureValue) 47 | } 48 | 49 | /** 50 | * If you have only an A and you wish to lift it into an OptionT[F,A], assuming you have an 51 | * Applicative instance for F you can use `some` which is an alias for `pure`. There also exists a 52 | * none method which can be used to create an OptionT[F,A], where the Option wrapped A type is 53 | * actually a None: 54 | * 55 | * {{{ 56 | * import scala.concurrent.Future 57 | * import scala.concurrent.ExecutionContext.Implicits.global 58 | * import cats.implicits._ //imports implicit Applicative[Future] 59 | * }}} 60 | */ 61 | def pureOption(spanishGreetValue: String, englishGreetValue: String, failedGreetValue: String) = { 62 | // given 63 | val englishGreetOpT: OptionT[Id, String] = OptionT.some[Id]("Hello") 64 | val spanishGreetOpT: OptionT[Future, String] = OptionT.pure[Future]("Hola") 65 | val failedGreetOpT: OptionT[Future, String] = OptionT.none 66 | 67 | // when 68 | val englishGreet: Id[String] = englishGreetOpT.getOrElse("") 69 | val spanishGreet: Future[String] = spanishGreetOpT.getOrElse("") 70 | val failedGreet: Future[String] = failedGreetOpT.getOrElse("") 71 | 72 | // then 73 | englishGreet shouldBe a[Id[_]] 74 | spanishGreet shouldBe a[Future[_]] 75 | englishGreet shouldBe (englishGreetValue: String) 76 | spanishGreet.futureValue shouldBe (spanishGreetValue: String) 77 | failedGreet.futureValue shouldBe (failedGreetValue: String) 78 | } 79 | 80 | /** 81 | * Sometimes you may have an Option[A] and/or F[A] and want to lift them into an OptionT[F, A]. 82 | * For this purpose OptionT exposes two useful methods, namely fromOption and liftF, and the 83 | * standard apply respectively. E.g.: 84 | * {{{ 85 | * val greetingFO: Future[Option[String]] = Future.successful(Some("Hello")) 86 | * 87 | * val firstnameF: Future[String] = Future.successful("Jane") 88 | * 89 | * val lastnameO: Option[String] = Some("Doe") 90 | * }}} 91 | */ 92 | def fromOptionT( 93 | maybeFrenchGreet: Option[String], 94 | maybeItalianGreet: Option[String], 95 | maybeFailedGreet: Option[String] 96 | ) = { 97 | // given 98 | val frenchGreetFO: Future[Option[String]] = Future.successful(Option("Bonjour")) 99 | val italianGreetF: Future[String] = Future.successful("Ciao") 100 | val failedGreetO: Option[String] = None 101 | 102 | // when 103 | val frenchGreetOpT: OptionT[Future, String] = OptionT(frenchGreetFO) 104 | val italianGreetOpT: OptionT[Future, String] = OptionT.liftF(italianGreetF) 105 | val failedGreetOpT: OptionT[Future, String] = OptionT.fromOption(failedGreetO) 106 | 107 | // then 108 | assert(frenchGreetOpT === OptionT.fromOption[Future](maybeFrenchGreet)) 109 | assert(italianGreetOpT === OptionT.fromOption[Future](maybeItalianGreet)) 110 | assert(failedGreetOpT === OptionT.fromOption[Future](maybeFailedGreet)) 111 | } 112 | 113 | /** 114 | * As you can see, the implementations of all of these variations are very similar. We want to 115 | * call the Option operation (map, filter, filterNot, getOrElse), but since our Option is wrapped 116 | * in a Future, we first need to map over the Future. OptionT can help remove some of this 117 | * boilerplate. It exposes methods that look like those on Option, but it handles the outer map 118 | * call on the Future so we don’t have to: 119 | * 120 | * {{{ 121 | * //operating with flattened Future[Option[String] 122 | * import scala.concurrent.Future 123 | * import scala.concurrent.ExecutionContext.Implicits.global 124 | * val customGreeting: Future[Option[String]] = Future.successful(Some("welcome back, Lola")) 125 | * val excitedGreeting: Future[Option[String]] = customGreeting.map(_.map(_ + "!")) 126 | * val hasWelcome: Future[Option[String]] = customGreeting.map(_.filter(_.contains("welcome"))) 127 | * val noWelcome: Future[Option[String]] = customGreeting.map(_.filterNot(_.contains("welcome"))) 128 | * val withFallback: Future[String] = customGreeting.map(_.getOrElse("hello, there!")) 129 | * 130 | * //operating with transformer OptionT[Future, String] 131 | * import cats.data.OptionT 132 | * import cats.implicits._ 133 | * val customGreetingT: OptionT[Future, String] = OptionT(customGreeting) 134 | * val excitedGreeting: OptionT[Future, String] = customGreetingT.map(_ + "!") 135 | * val withWelcome: OptionT[Future, String] = customGreetingT.filter(_.contains("welcome")) 136 | * val noWelcome: OptionT[Future, String] = customGreetingT.filterNot(_.contains("welcome")) //None 137 | * val withFallback: Future[String] = customGreetingT.getOrElse("hello, there!") 138 | * }}} 139 | */ 140 | def optionTMethods( 141 | maybeGreet: List[Option[String]], 142 | maybeGreetWorld: List[Option[String]], 143 | maybeGreetH: List[Option[String]], 144 | maybeGreetGuten: List[Option[String]], 145 | maybeGreetGutenTag: List[String], 146 | maybeGreetGutenAbend: List[String] 147 | ) = { 148 | // given 149 | val germanGreetingsLO: List[Option[String]] = 150 | List("Hallo".some, "Hi".some, "Guten Morgen".some, none[String]) 151 | val germanGreetingsT: OptionT[List, String] = OptionT(germanGreetingsLO) 152 | 153 | // when/then (optionT vs flatten) 154 | // map 155 | germanGreetingsT.map(_ + "!") shouldBe OptionT(maybeGreet: List[Option[String]]) 156 | germanGreetingsLO.map(_.map(_ + " World!")) shouldBe OptionT( 157 | maybeGreetWorld: List[Option[String]] 158 | ).value 159 | // filter 160 | germanGreetingsT.filter(_.contains("H")) shouldBe OptionT(maybeGreetH: List[Option[String]]) 161 | germanGreetingsLO.map(_.filter(_.contains("Guten"))) shouldBe OptionT( 162 | maybeGreetGuten: List[Option[String]] 163 | ).value 164 | // getOrElse 165 | germanGreetingsT.getOrElse("Guten Tag") shouldBe (maybeGreetGutenTag: List[String]) 166 | germanGreetingsLO.map(_.getOrElse("Guten Abend")) shouldBe (maybeGreetGutenAbend: List[String]) 167 | // ... 168 | } 169 | 170 | /** 171 | * OptionT is a Monad, so it has a flatMap method which can be used in a for-comprehension. 172 | * 173 | * {{{ 174 | * val greetingFO: Future[Option[String]] = Future.successful(Some("Hello")) 175 | * 176 | * val firstnameF: Future[String] = Future.successful("Jane") 177 | * 178 | * val lastnameO: Option[String] = Some("Doe") 179 | * 180 | * val ot: OptionT[Future, String] = for { 181 | * g <- OptionT(greetingFO) 182 | * f <- OptionT.liftF(firstnameF) 183 | * l <- OptionT.fromOption[Future](lastnameO) 184 | * } yield s"$g $f $l" 185 | * 186 | * val result: Future[Option[String]] = ot.value // Future(Some("Hello Jane Doe")) 187 | * }}} 188 | */ 189 | def forComprehension(maybeOpResult: Option[Double]) = { 190 | // given 191 | val initialNumber: Future[Option[Double]] = Future(0.0.some) 192 | 193 | // when 194 | val opResult: OptionT[Future, Double] = for { 195 | a <- OptionT(initialNumber).map(_ + 2) 196 | b <- OptionT.liftF(Future(a)).map(_ * 6) 197 | c <- OptionT.liftF(Future(b)).map(_ / 8) 198 | } yield c 199 | 200 | // then 201 | opResult.value.futureValue shouldBe (maybeOpResult: Option[Double]) 202 | 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Semigroup.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats.kernel.Semigroup 20 | import org.scalatest.matchers.should.Matchers 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | 23 | /** 24 | * A semigroup for some given type A has a single operation (which we will call `combine`), which 25 | * takes two values of type A, and returns a value of type A. This operation must be guaranteed to 26 | * be associative. That is to say that: 27 | * 28 | * {{{ 29 | * ((a combine b) combine c) 30 | * }}} 31 | * 32 | * must be the same as 33 | * 34 | * {{{ 35 | * (a combine (b combine c)) 36 | * }}} 37 | * 38 | * for all possible values of a,b,c. 39 | * 40 | * There are instances of `Semigroup` defined for many types found in the scala common library. For 41 | * example, `Int` values are combined using addition by default but multiplication is also 42 | * associative and forms another `Semigroup`. 43 | * 44 | * {{{ 45 | * import cats.Semigroup 46 | * }}} 47 | * 48 | * @param name 49 | * semigroup 50 | */ 51 | object SemigroupSection 52 | extends AnyFlatSpec 53 | with Matchers 54 | with org.scalaexercises.definitions.Section { 55 | 56 | /** 57 | * Now that you've learned about the `Semigroup` instance for `Int` try to guess how it works in 58 | * the following examples: 59 | */ 60 | def semigroupCombine(res0: Int, res1: List[Int], res2: Option[Int], res3: Option[Int]) = { 61 | import cats.implicits._ 62 | 63 | Semigroup[Int].combine(1, 2) should be(res0) 64 | Semigroup[List[Int]].combine(List(1, 2, 3), List(4, 5, 6)) should be(res1) 65 | Semigroup[Option[Int]].combine(Option(1), Option(2)) should be(res2) 66 | Semigroup[Option[Int]].combine(Option(1), None) should be(res3) 67 | } 68 | 69 | /** 70 | * And now try a slightly more complex combination: 71 | */ 72 | def semigroupCombineComplex(res0: Int) = { 73 | import cats.implicits._ 74 | 75 | Semigroup[Int => Int].combine(_ + 1, _ * 10).apply(6) should be(res0) 76 | 77 | } 78 | 79 | /** 80 | * Many of these types have methods defined directly on them, which allow for such combining, e.g. 81 | * `++` on List, but the value of having a `Semigroup` type class available is that these compose, 82 | * so for instance, we can say 83 | * 84 | * {{{ 85 | * Map("foo" -> Map("bar" -> 5)).combine(Map("foo" -> Map("bar" -> 6), "baz" -> Map())) 86 | * Map("foo" -> List(1, 2)).combine(Map("foo" -> List(3,4), "bar" -> List(42))) 87 | * }}} 88 | * 89 | * which is far more likely to be useful than 90 | * 91 | * {{{ 92 | * Map("foo" -> Map("bar" -> 5)) ++ Map("foo" -> Map("bar" -> 6), "baz" -> Map()) 93 | * Map("foo" -> List(1, 2)) ++ Map("foo" -> List(3,4), "bar" -> List(42)) 94 | * }}} 95 | * 96 | * since the first version uses the Semigroup's `combine` operation, it will merge the map's 97 | * values with `combine`. 98 | */ 99 | def composingSemigroups(res0: Map[String, Int]) = { 100 | import cats.implicits._ 101 | 102 | val aMap = Map("foo" -> Map("bar" -> 5)) 103 | val anotherMap = Map("foo" -> Map("bar" -> 6)) 104 | val combinedMap = Semigroup[Map[String, Map[String, Int]]].combine(aMap, anotherMap) 105 | 106 | combinedMap.get("foo") should be(Some(res0)) 107 | } 108 | 109 | /** 110 | * There is inline syntax available for `Semigroup`. Here we are following the convention from 111 | * scalaz, that `|+|` is the operator from `Semigroup`. 112 | * 113 | * You'll notice that instead of declaring `one` as `Some(1)`, I chose `Option(1)`, and I added an 114 | * explicit type declaration for `n`. This is because there aren't type class instances for Some 115 | * or None, but for Option. 116 | */ 117 | def semigroupSpecialSyntax( 118 | res0: Option[Int], 119 | res1: Option[Int], 120 | res2: Option[Int], 121 | res3: Option[Int] 122 | ) = { 123 | import cats.implicits._ 124 | 125 | val one: Option[Int] = Option(1) 126 | val two: Option[Int] = Option(2) 127 | val n: Option[Int] = None 128 | 129 | one |+| two should be(res0) 130 | n |+| two should be(res1) 131 | n |+| n should be(res2) 132 | two |+| n should be(res3) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Traverse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats.implicits._ 23 | 24 | import TraverseHelpers._ 25 | 26 | /** 27 | * In functional programming it is very common to encode "effects" as data types - common effects 28 | * include `Option` for possibly missing values, `Either` and `Validated` for possible errors, and 29 | * `Future` for asynchronous computations. 30 | * 31 | * These effects tend to show up in functions working on a single piece of data - for instance 32 | * parsing a single `String` into an `Int`, validating a login, or asynchronously fetching website 33 | * information for a user. 34 | * 35 | * {{{ 36 | * import scala.concurrent.Future 37 | * 38 | * def parseInt(s: String): Option[Int] = ??? 39 | * 40 | * trait SecurityError 41 | * trait Credentials 42 | * 43 | * def validateLogin(cred: Credentials): Either[SecurityError, Unit] = ??? 44 | * 45 | * trait Profile 46 | * trait User 47 | * 48 | * def userInfo(user: User): Future[Profile] = ??? 49 | * }}} 50 | * 51 | * Each function asks only for the data it actually needs; in the case of `userInfo`, a single 52 | * `User`. We certainly could write one that takes a `List[User]` and fetch the profile for all of 53 | * them, though it would be a bit strange since fetching a single user would require us to either 54 | * wrap it in a `List`, or write a separate function that takes in a single user anyways. More 55 | * fundamentally, functional programming is about building lots of small, independent pieces and 56 | * composing them to make larger and larger pieces - does this hold true in this case? 57 | * 58 | * Given just `User => Future[Profile]`, what should we do if we want to fetch profiles for a 59 | * `List[User]`? We could try familiar combinators like `map`. 60 | * 61 | * {{{ 62 | * def profilesFor(users: List[User]): List[Future[Profile]] = users.map(userInfo) 63 | * }}} 64 | * 65 | * Note the return type `List[Future[Profile]]`. This makes sense given the type signatures, but 66 | * seems unwieldy. We now have a list of asynchronous values, and to work with those values we must 67 | * then use the combinators on `Future` for every single one. It would be nicer instead if we could 68 | * get the aggregate result in a single `Future`, say a `Future[List[Profile]]`. 69 | * 70 | * As it turns out, the `Future` companion object has a `traverse` method on it. However, that 71 | * method is specialized to standard library collections and `Future`s - there exists a much more 72 | * generalized form that would allow us to parse a `List[String]` or validate credentials for a 73 | * `List[User]`. 74 | * 75 | * Enter `Traverse`. 76 | * 77 | * =The type class= 78 | * At center stage of `Traverse` is the `traverse` method. 79 | * 80 | * {{{ 81 | * trait Traverse[F[_]] { 82 | * def traverse[G[_] : Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] 83 | * } 84 | * }}} 85 | * 86 | * In our above example, `F` is `List`, and `G` is `Option`, `Either`, or `Future`. For the profile 87 | * example, `traverse` says given a `List[User]` and a function `User => Future[Profile]`, it can 88 | * give you a `Future[List[Profile]]`. 89 | * 90 | * Abstracting away the `G` (still imagining `F` to be `List`), `traverse` says given a collection 91 | * of data, and a function that takes a piece of data and returns an effectful value, it will 92 | * traverse the collection, applying the function and aggregating the effectful values (in a `List`) 93 | * as it goes. 94 | * 95 | * In the most general form, `F[_]` is some sort of context which may contain a value (or several). 96 | * While `List` tends to be among the most general cases, there also exist `Traverse` instances for 97 | * `Option`, `Either`, and `Validated` (among others). 98 | * 99 | * @param name 100 | * traverse 101 | */ 102 | object TraverseSection 103 | extends AnyFlatSpec 104 | with Matchers 105 | with org.scalaexercises.definitions.Section { 106 | 107 | /** 108 | * ==Choose your effect== 109 | * 110 | * The type signature of `Traverse` appears highly abstract, and indeed it is - what `traverse` 111 | * does as it walks the `F[A]` depends on the effect of the function. Let's see some examples 112 | * where `F` is taken to be `List`. 113 | * 114 | * {{{ 115 | * import cats.Semigroup 116 | * import cats.data.{NonEmptyList, OneAnd, Validated, ValidatedNel} 117 | * import cats.implicits._ 118 | * 119 | * def parseIntEither(s: String): Either[NumberFormatException, Int] = 120 | * Either.catchOnly[NumberFormatException](s.toInt) 121 | * 122 | * def parseIntValidated(s: String): ValidatedNel[NumberFormatException, Int] = 123 | * Validated.catchOnly[NumberFormatException](s.toInt).toValidatedNel 124 | * }}} 125 | * 126 | * We can now traverse structures that contain strings parsing them into integers and accumulating 127 | * failures with `Either`. 128 | */ 129 | def traverseuFunction(res0: List[Int], res1: Boolean) = { 130 | List("1", "2", "3").traverse(parseIntEither) should be(Right(res0)) 131 | List("1", "abc", "3").traverse(parseIntEither).isLeft should be(res1) 132 | } 133 | 134 | /** 135 | * We need proof that `NonEmptyList[A]` is a `Semigroup `for there to be an `Applicative` instance 136 | * for `ValidatedNel`. 137 | * 138 | * {{{ 139 | * implicit def nelSemigroup[A]: Semigroup[NonEmptyList[A]] = 140 | * OneAnd.oneAndSemigroupK[List].algebra[A] 141 | * }}} 142 | * 143 | * Now that we've provided such evidence, we can use `ValidatedNel` as an applicative. 144 | */ 145 | def traverseuValidated(res0: Boolean) = 146 | List("1", "2", "3").traverse(parseIntValidated).isValid should be(res0) 147 | 148 | /** 149 | * Notice that in the `Either` case, should any string fail to parse the entire traversal is 150 | * considered a failure. Moreover, once it hits its first bad parse, it will not attempt to parse 151 | * any others down the line (similar behavior would be found with using `Option` as the effect). 152 | * Contrast this with `Validated` where even if one bad parse is hit, it will continue trying to 153 | * parse the others, accumulating any and all errors as it goes. The behavior of traversal is 154 | * closely tied with the `Applicative` behavior of the data type. 155 | * 156 | * Going back to our `Future` example, we can write an `Applicative` instance for `Future` that 157 | * runs each `Future` concurrently. Then when we traverse a `List[A]` with an `A => Future[B]`, we 158 | * can imagine the traversal as a scatter-gather. Each `A` creates a concurrent computation that 159 | * will produce a `B` (the scatter), and as the `Future`s complete they will be gathered back into 160 | * a `List`. 161 | * 162 | * ==Playing with `Reader`== 163 | * 164 | * Another interesting effect we can use is `Reader`. Recall that a `Reader[E, A]` is a type alias 165 | * for `Kleisli[Id, E, A]` which is a wrapper around `E => A`. 166 | * 167 | * If we fix `E` to be some sort of environment or configuration, we can use the `Reader` 168 | * applicative in our traverse. 169 | * 170 | * {{{ 171 | * import cats.data.Reader 172 | * 173 | * trait Context 174 | * trait Topic 175 | * trait Result 176 | * 177 | * type Job[A] = Reader[Context, A] 178 | * 179 | * def processTopic(topic: Topic): Job[Result] = ??? 180 | * }}} 181 | * 182 | * We can imagine we have a data pipeline that processes a bunch of data, each piece of data being 183 | * categorized by a topic. Given a specific topic, we produce a `Job` that processes that topic. 184 | * (Note that since a `Job` is just a `Reader`/`Kleisli`, one could write many small `Job`s and 185 | * compose them together into one `Job` that is used/returned by `processTopic`.) 186 | * 187 | * Corresponding to our bunches of data are bunches of topics, a `List[Topic]` if you will. Since 188 | * `Reader` has an `Applicative` instance, we can `traverse` over this list with `processTopic`. 189 | * 190 | * {{{ 191 | * def processTopics(topics: List[Topic]) = 192 | * topics.traverse(processTopic) 193 | * }}} 194 | * 195 | * Note the nice return type - `Job[List[Result]]`. We now have one aggregate `Job` that when run, 196 | * will go through each topic and run the topic-specific job, collecting results as it goes. We 197 | * say "when run" because a `Job` is some function that requires a `Context` before producing the 198 | * value we want. 199 | * 200 | * One example of a "context" can be found in the [[http://spark.apache.org/ Spark]] project. In 201 | * Spark, information needed to run a Spark job (where the master node is, memory allocated, etc.) 202 | * resides in a `SparkContext`. Going back to the above example, we can see how one may define 203 | * topic-specific Spark jobs (`type Job[A] = Reader[SparkContext, A]`) and then run several Spark 204 | * jobs on a collection of topics via `traverse`. We then get back a `Job[List[Result]]`, which is 205 | * equivalent to `SparkContext => List[Result]`. When finally passed a `SparkContext`, we can run 206 | * the job and get our results back. 207 | * 208 | * Moreover, the fact that our aggregate job is not tied to any specific `SparkContext` allows us 209 | * to pass in a `SparkContext` pointing to a production cluster, or (using the exact same job) 210 | * pass in a test `SparkContext` that just runs locally across threads. This makes testing our 211 | * large job nice and easy. 212 | * 213 | * Finally, this encoding ensures that all the jobs for each topic run on the exact same cluster. 214 | * At no point do we manually pass in or thread a `SparkContext` through - that is taken care for 215 | * us by the (applicative) effect of `Reader` and therefore by `traverse`. 216 | * 217 | * =Sequencing= 218 | * 219 | * Sometimes you may find yourself with a collection of data, each of which is already in an 220 | * effect, for instance a `List[Option[A]]`. To make this easier to work with, you want a 221 | * `Option[List[A]]`. Given `Option` has an `Applicative` instance, we can traverse over the list 222 | * with the identity function. 223 | */ 224 | def sequencing(res0: Option[List[Int]], res1: Option[List[Int]]) = { 225 | import cats.implicits._ 226 | 227 | List(Option(1), Option(2), Option(3)).traverse(identity) should be(res0) 228 | List(Option(1), None, Option(3)).traverse(identity) should be(res1) 229 | } 230 | 231 | /** 232 | * `Traverse` provides a convenience method `sequence` that does exactly this. 233 | * 234 | * {{{ 235 | * List(Option(1), Option(2), Option(3)).sequence 236 | * List(Option(1), None, Option(3)).sequence 237 | * }}} 238 | * 239 | * =Traversing for effect= 240 | * 241 | * Sometimes our effectful functions return a `Unit` value in cases where there is no interesting 242 | * value to return (e.g. writing to some sort of store). 243 | * 244 | * {{{ 245 | * trait Data 246 | * def writeToStore(data: Data): Future[Unit] = ??? 247 | * }}} 248 | * 249 | * If we traverse using this, we end up with a funny type. 250 | * 251 | * {{{ 252 | * import cats.implicits._ 253 | * import scala.concurrent.ExecutionContext.Implicits.global 254 | * 255 | * def writeManyToStore(data: List[Data]) = 256 | * data.traverse(writeToStore) 257 | * }}} 258 | * 259 | * We end up with a `Future[List[Unit]]`! A `List[Unit]` is not of any use to us, and communicates 260 | * the same amount of information as a single `Unit` does. 261 | * 262 | * Traversing solely for the sake of the effect (ignoring any values that may be produced, `Unit` 263 | * or otherwise) is common, so `Foldable` (superclass of `Traverse`) provides `traverse_` and 264 | * `sequence_` methods that do the same thing as `traverse` and `sequence` but ignores any value 265 | * produced along the way, returning `Unit` at the end. 266 | */ 267 | def traversingForEffects(res0: Option[Unit], res1: Option[Unit]) = { 268 | import cats.implicits._ 269 | 270 | List(Option(1), Option(2), Option(3)).sequence_ should be(res0) 271 | List(Option(1), None, Option(3)).sequence_ should be(res1) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/main/scala/catslib/TraverseHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | object TraverseHelpers { 20 | import cats.data.{Validated, ValidatedNel} 21 | import cats.implicits._ 22 | 23 | def parseIntEither(s: String): Either[NumberFormatException, Int] = 24 | Either.catchOnly[NumberFormatException](s.toInt) 25 | 26 | def parseIntValidated(s: String): ValidatedNel[NumberFormatException, Int] = 27 | Validated.catchOnly[NumberFormatException](s.toInt).toValidatedNel 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/catslib/Validated.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalatest.matchers.should.Matchers 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | 22 | import cats.data.Validated 23 | 24 | import ValidatedHelpers._ 25 | 26 | /** 27 | * Imagine you are filling out a web form to sign up for an account. You input your username and 28 | * password and submit. Response comes back saying your username can't have dashes in it, so you 29 | * make some changes and resubmit. Can't have special characters either. Change, resubmit. Passwords 30 | * need to have at least one capital letter. Change, resubmit. Password needs to have at least one 31 | * number. 32 | * 33 | * Or perhaps you're reading from a configuration file. One could imagine the configuration library 34 | * you're using returns a `scala.util.Try`, or maybe a `scala.util.Either`. Your parsing may look 35 | * something like: 36 | * 37 | * {{{ 38 | * case class ConnectionParams(url: String, port: Int) 39 | * 40 | * for { 41 | * url <- config[String]("url") 42 | * port <- config[Int]("port") 43 | * } yield ConnectionParams(url, port) 44 | * }}} 45 | * 46 | * You run your program and it says key "url" not found, turns out the key was "endpoint". So you 47 | * change your code and re-run. Now it says the "port" key was not a well-formed integer. 48 | * 49 | * It would be nice to have all of these errors reported simultaneously. That the username can't 50 | * have dashes can be validated separately from it not having special characters, as well as from 51 | * the password needing to have certain requirements. A misspelled (or missing) field in a config 52 | * can be validated separately from another field not being well-formed. 53 | * 54 | * Enter `Validated`. 55 | * 56 | * =Parallel validation= 57 | * 58 | * Our goal is to report any and all errors across independent bits of data. For instance, when we 59 | * ask for several pieces of configuration, each configuration field can be validated separately 60 | * from one another. How then do we enforce that the data we are working with is independent? We ask 61 | * for both of them up front. 62 | * 63 | * As our running example, we will look at config parsing. Our config will be represented by a 64 | * `Map[String, String]`. Parsing will be handled by a `Read` type class - we provide instances only 65 | * for `String` and `Int` for brevity. 66 | * 67 | * {{{ 68 | * trait Read[A] { 69 | * def read(s: String): Option[A] 70 | * } 71 | * 72 | * object Read { 73 | * def apply[A](implicit A: Read[A]): Read[A] = A 74 | * 75 | * implicit val stringRead: Read[String] = 76 | * new Read[String] { def read(s: String): Option[String] = Option(s) } 77 | * 78 | * implicit val intRead: Read[Int] = 79 | * new Read[Int] { 80 | * def read(s: String): Option[Int] = 81 | * if (s.matches("-?[0-9]+")) Some(s.toInt) 82 | * else None 83 | * } 84 | * } 85 | * }}} 86 | * 87 | * Then we enumerate our errors—when asking for a config value, one of two things can go wrong: the 88 | * field is missing, or it is not well-formed with regards to the expected type. 89 | * 90 | * {{{ 91 | * sealed abstract class ConfigError 92 | * final case class MissingConfig(field: String) extends ConfigError 93 | * final case class ParseError(field: String) extends ConfigError 94 | * }}} 95 | * 96 | * We need a data type that can represent either a successful value (a parsed configuration), or an 97 | * error. It would look like the following, which cats provides in `cats.data.Validated`: 98 | * 99 | * {{{ 100 | * sealed abstract class Validated[+E, +A] 101 | * 102 | * object Validated { 103 | * final case class Valid[+A](a: A) extends Validated[Nothing, A] 104 | * final case class Invalid[+E](e: E) extends Validated[E, Nothing] 105 | * } 106 | * }}} 107 | * 108 | * Now we are ready to write our parser. 109 | * 110 | * {{{ 111 | * import cats.data.Validated 112 | * import cats.data.Validated.{Invalid, Valid} 113 | * 114 | * case class Config(map: Map[String, String]) { 115 | * def parse[A : Read](key: String): Validated[ConfigError, A] = 116 | * map.get(key) match { 117 | * case None => Invalid(MissingConfig(key)) 118 | * case Some(value) => 119 | * Read[A].read(value) match { 120 | * case None => Invalid(ParseError(key)) 121 | * case Some(a) => Valid(a) 122 | * } 123 | * } 124 | * } 125 | * }}} 126 | * 127 | * Everything is in place to write the parallel validator. Recall that we can only do parallel 128 | * validation if each piece is independent. How do we enforce the data is independent? By asking for 129 | * all of it up front. Let's start with two pieces of data. 130 | * 131 | * {{{ 132 | * def parallelValidate[E, A, B, C](v1: Validated[E, A], v2: Validated[E, B])(f: (A, B) => C): Validated[E, C] = 133 | * (v1, v2) match { 134 | * case (Valid(a), Valid(b)) => Valid(f(a, b)) 135 | * case (Valid(_), i@Invalid(_)) => i 136 | * case (i@Invalid(_), Valid(_)) => i 137 | * case (Invalid(e1), Invalid(e2)) => ??? 138 | * } 139 | * }}} 140 | * 141 | * We've run into a problem. In the case where both have errors, we want to report both. But we have 142 | * no way of combining the two errors into one error! Perhaps we can put both errors into a `List`, 143 | * but that seems needlessly specific—clients may want to define their own way of combining errors. 144 | * 145 | * How then do we abstract over a binary operation? The `Semigroup` type class captures this idea. 146 | * 147 | * {{{ 148 | * import cats.Semigroup 149 | * 150 | * def parallelValidate[E : Semigroup, A, B, C](v1: Validated[E, A], v2: Validated[E, B])(f: (A, B) => C): Validated[E, C] = 151 | * (v1, v2) match { 152 | * case (Valid(a), Valid(b)) => Valid(f(a, b)) 153 | * case (Valid(_), i@Invalid(_)) => i 154 | * case (i@Invalid(_), Valid(_)) => i 155 | * case (Invalid(e1), Invalid(e2)) => Invalid(Semigroup[E].combine(e1, e2)) 156 | * } 157 | * }}} 158 | * 159 | * Perfect! But, going back to our example, we don't have a way to combine `ConfigError`s. But as 160 | * clients, we can change our `Validated` values where the error can be combined, say, a 161 | * `List[ConfigError]`. It is more common however to use a `NonEmptyList[ConfigError]`—the 162 | * `NonEmptyList` statically guarantees we have at least one value, which aligns with the fact that 163 | * if we have an `Invalid`, then we most certainly have at least one error. This technique is so 164 | * common there is a convenient method on `Validated` called `toValidatedNel` that turns any 165 | * `Validated[E, A]` value to a `Validated[NonEmptyList[E], A]`. Additionally, the type alias 166 | * `ValidatedNel[E, A]` is provided. 167 | * 168 | * Time to parse. 169 | * 170 | * {{{ 171 | * import cats.SemigroupK 172 | * import cats.data.NonEmptyList 173 | * import cats.implicits._ 174 | * 175 | * implicit val nelSemigroup: Semigroup[NonEmptyList[ConfigError]] = 176 | * SemigroupK[NonEmptyList].algebra[ConfigError] 177 | * 178 | * implicit val readString: Read[String] = Read.stringRead 179 | * implicit val readInt: Read[Int] = Read.intRead 180 | * }}} 181 | * 182 | * @param name 183 | * validated 184 | */ 185 | object ValidatedSection 186 | extends AnyFlatSpec 187 | with Matchers 188 | with org.scalaexercises.definitions.Section { 189 | 190 | /** 191 | * When no errors are present in the configuration, we get a `ConnectionParams` wrapped in a 192 | * `Valid` instance. 193 | */ 194 | def noErrors(res0: Boolean, res1: String, res2: Int) = { 195 | val config = Config(Map(("url", "127.0.0.1"), ("port", "1337"))) 196 | 197 | val valid = parallelValidate( 198 | config.parse[String]("url").toValidatedNel, 199 | config.parse[Int]("port").toValidatedNel 200 | )(ConnectionParams.apply) 201 | 202 | valid.isValid should be(res0) 203 | valid.getOrElse(ConnectionParams("", 0)) should be(ConnectionParams(res1, res2)) 204 | } 205 | 206 | /** 207 | * But what happens when having one or more errors? They are accumulated in a `NonEmptyList` 208 | * wrapped in an `Invalid` instance. 209 | */ 210 | def someErrors(res0: Boolean, res1: Boolean) = { 211 | val config = Config(Map(("endpoint", "127.0.0.1"), ("port", "not a number"))) 212 | 213 | val invalid = parallelValidate( 214 | config.parse[String]("url").toValidatedNel, 215 | config.parse[Int]("port").toValidatedNel 216 | )(ConnectionParams.apply) 217 | 218 | import cats.data.Validated 219 | import cats.data.NonEmptyList 220 | 221 | invalid.isValid should be(res0) 222 | val errors = NonEmptyList(MissingConfig("url"), List(ParseError("port"))) 223 | invalid == Validated.invalid(errors) should be(res1) 224 | } 225 | 226 | /** 227 | * =Apply= 228 | * 229 | * Our `parallelValidate` function looks awfully like the `Apply#map2` function. 230 | * 231 | * {{{ 232 | * def map2[F[_], A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] 233 | * }}} 234 | * 235 | * Which can be defined in terms of `Apply#ap` and `Apply#map`, the very functions needed to 236 | * create an `Apply` instance. 237 | * 238 | * Can we perhaps define an `Apply` instance for `Validated`? Better yet, can we define an 239 | * `Applicative` instance? 240 | * 241 | * {{{ 242 | * import cats.Applicative 243 | * 244 | * implicit def validatedApplicative[E : Semigroup]: Applicative[Validated[E, *]] = 245 | * new Applicative[Validated[E, *]] { 246 | * def ap[A, B](f: Validated[E, A => B])(fa: Validated[E, A]): Validated[E, B] = 247 | * (fa, f) match { 248 | * case (Valid(a), Valid(fab)) => Valid(fab(a)) 249 | * case (i@Invalid(_), Valid(_)) => i 250 | * case (Valid(_), i@Invalid(_)) => i 251 | * case (Invalid(e1), Invalid(e2)) => Invalid(Semigroup[E].combine(e1, e2)) 252 | * } 253 | * 254 | * def pure[A](x: A): Validated[E, A] = Validated.valid(x) 255 | * def map[A, B](fa: Validated[E, A])(f: A => B): Validated[E, B] = fa.map(f) 256 | * def product[A, B](fa: Validated[E, A], fb: Validated[E, B]): Validated[E, (A, B)] = 257 | * ap(fa.map(a => (b: B) => (a, b)))(fb) 258 | * } 259 | * }}} 260 | * 261 | * Awesome! And now we also get access to all the goodness of `Applicative`, which includes 262 | * `map{2-22}`, as well as the `Cartesian` syntax `|@|`. 263 | * 264 | * We can now easily ask for several bits of configuration and get any and all errors returned 265 | * back. 266 | * 267 | * {{{ 268 | * import cats.Apply 269 | * import cats.data.ValidatedNel 270 | * 271 | * implicit val nelSemigroup: Semigroup[NonEmptyList[ConfigError]] = 272 | * SemigroupK[NonEmptyList].algebra[ConfigError] 273 | * 274 | * val config = Config(Map(("name", "cat"), ("age", "not a number"), ("houseNumber", "1234"), ("lane", "feline street"))) 275 | * 276 | * case class Address(houseNumber: Int, street: String) 277 | * case class Person(name: String, age: Int, address: Address) 278 | * }}} 279 | * 280 | * Thus. 281 | * 282 | * {{{ 283 | * val personFromConfig: ValidatedNel[ConfigError, Person] = 284 | * Apply[ValidatedNel[ConfigError, *]].map4(config.parse[String]("name").toValidatedNel, 285 | * config.parse[Int]("age").toValidatedNel, 286 | * config.parse[Int]("house_number").toValidatedNel, 287 | * config.parse[String]("street").toValidatedNel) { 288 | * case (name, age, houseNumber, street) => Person(name, age, Address(houseNumber, street)) 289 | * } 290 | * }}} 291 | * 292 | * We can now rewrite validations in terms of `Apply`. 293 | * 294 | * ==Of `flatMap`s and `Either`s== 295 | * 296 | * `Option` has `flatMap`, `Either` has `flatMap`, where's `Validated`'s? Let's try to implement 297 | * it—better yet, let's implement the `Monad` type class. 298 | * 299 | * {{{ 300 | * import cats.Monad 301 | * 302 | * implicit def validatedMonad[E]: Monad[Validated[E, *]] = 303 | * new Monad[Validated[E, *]] { 304 | * def flatMap[A, B](fa: Validated[E, A])(f: A => Validated[E, B]): Validated[E, B] = 305 | * fa match { 306 | * case Valid(a) => f(a) 307 | * case i@Invalid(_) => i 308 | * } 309 | * 310 | * def pure[A](x: A): Validated[E, A] = Valid(x) 311 | * } 312 | * }}} 313 | * 314 | * Note that all `Monad` instances are also `Applicative` instances, where `ap` is defined as 315 | * 316 | * {{{ 317 | * trait Monad[F[_]] { 318 | * def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] 319 | * def pure[A](x: A): F[A] 320 | * 321 | * def map[A, B](fa: F[A])(f: A => B): F[B] = 322 | * flatMap(fa)(f.andThen(pure)) 323 | * 324 | * def ap[A, B](fa: F[A])(f: F[A => B]): F[B] = 325 | * flatMap(fa)(a => map(f)(fab => fab(a))) 326 | * } 327 | * }}} 328 | * 329 | * However, the `ap` behavior defined in terms of `flatMap` does not behave the same as that of 330 | * our `ap` defined above. Observe: 331 | * 332 | * {{{ 333 | * val v = validatedMonad.tuple2(Validated.invalidNel[String, Int]("oops"), Validated.invalidNel[String, Double]("uh oh")) 334 | * }}} 335 | * 336 | * This one short circuits! Therefore, if we were to define a `Monad` (or `FlatMap`) instance for 337 | * `Validated` we would have to override `ap` to get the behavior we want. But then the behavior 338 | * of `flatMap` would be inconsistent with that of `ap`, not good. Therefore, `Validated` has only 339 | * an `Applicative` instance. 340 | * 341 | * =Sequential Validation= 342 | * 343 | * If you do want error accumulation but occasionally run into places where sequential validation 344 | * is needed, then `Validated` provides a couple methods that may be helpful. 345 | * 346 | * \== `andThen` === 347 | * 348 | * The `andThen` method is similar to `flatMap` (such as `Either.flatMap`). In the case of 349 | * success, it passes the valid value into a function that returns a new `Validated` instance. 350 | * 351 | * {{{ 352 | * val houseNumber = config.parse[Int]("house_number").andThen{ n => 353 | * if (n >= 0) Validated.valid(n) 354 | * else Validated.invalid(ParseError("house_number")) 355 | * } 356 | * }}} 357 | */ 358 | def sequentialValidation(res0: Boolean, res1: Boolean) = { 359 | val config = Config(Map("house_number" -> "-42")) 360 | 361 | val houseNumber = config.parse[Int]("house_number").andThen { n => 362 | if (n >= 0) Validated.valid(n) 363 | else Validated.invalid(ParseError("house_number")) 364 | } 365 | 366 | houseNumber.isValid should be(res0) 367 | val error = ParseError("house_number") 368 | houseNumber == Validated.invalid(error) should be(res1) 369 | } 370 | 371 | /** 372 | * ==`withEither`== 373 | * 374 | * The `withEither` method allows you to temporarily turn a `Validated` instance into an `Either` 375 | * instance and apply it to a function. 376 | * 377 | * {{{ 378 | * import cats.data.Either 379 | * 380 | * def positive(field: String, i: Int): Either[ConfigError, Int] = { 381 | * if (i >= 0) Either.right(i) 382 | * else Either.left(ParseError(field)) 383 | * } 384 | * }}} 385 | * 386 | * So we can get `Either`'s short-circuiting behaviour when using the `Validated` type. 387 | */ 388 | def validatedAsEither(res0: Boolean, res1: Boolean) = { 389 | val config = Config(Map("house_number" -> "-42")) 390 | 391 | val houseNumber = config.parse[Int]("house_number").withEither { 392 | either: Either[ConfigError, Int] => either.flatMap(i => positive("house_number", i)) 393 | } 394 | 395 | houseNumber.isValid should be(res0) 396 | val error = ParseError("house_number") 397 | houseNumber == Validated.invalid(error) should be(res1) 398 | } 399 | 400 | } 401 | -------------------------------------------------------------------------------- /src/main/scala/catslib/ValidatedHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | object ValidatedHelpers { 20 | case class ConnectionParams(url: String, port: Int) 21 | 22 | trait Read[A] { 23 | def read(s: String): Option[A] 24 | } 25 | 26 | object Read { 27 | def apply[A](implicit A: Read[A]): Read[A] = A 28 | 29 | implicit val stringRead: Read[String] = 30 | new Read[String] { def read(s: String): Option[String] = Some(s) } 31 | 32 | implicit val intRead: Read[Int] = 33 | new Read[Int] { 34 | def read(s: String): Option[Int] = 35 | if (s.matches("-?[0-9]+")) Some(s.toInt) 36 | else None 37 | } 38 | } 39 | 40 | sealed abstract class ConfigError 41 | final case class MissingConfig(field: String) extends ConfigError 42 | final case class ParseError(field: String) extends ConfigError 43 | 44 | import cats.data.Validated 45 | import cats.data.Validated.{Invalid, Valid} 46 | 47 | case class Config(map: Map[String, String]) { 48 | def parse[A: Read](key: String): Validated[ConfigError, A] = 49 | map.get(key) match { 50 | case None => Invalid(MissingConfig(key)) 51 | case Some(value) => 52 | Read[A].read(value) match { 53 | case None => Invalid(ParseError(key)) 54 | case Some(a) => Valid(a) 55 | } 56 | } 57 | } 58 | 59 | import cats.Semigroup 60 | 61 | def parallelValidate[E: Semigroup, A, B, C](v1: Validated[E, A], v2: Validated[E, B])( 62 | f: (A, B) => C 63 | ): Validated[E, C] = 64 | (v1, v2) match { 65 | case (Valid(a), Valid(b)) => Valid(f(a, b)) 66 | case (Valid(_), i @ Invalid(_)) => i 67 | case (i @ Invalid(_), Valid(_)) => i 68 | case (Invalid(e1), Invalid(e2)) => Invalid(Semigroup[E].combine(e1, e2)) 69 | } 70 | 71 | import cats.SemigroupK 72 | import cats.data.NonEmptyList 73 | import cats.implicits._ 74 | 75 | implicit val nelSemigroup: Semigroup[NonEmptyList[ConfigError]] = 76 | SemigroupK[NonEmptyList].algebra[ConfigError] 77 | 78 | implicit val readString: Read[String] = Read.stringRead 79 | implicit val readInt: Read[Int] = Read.intRead 80 | 81 | def positive(field: String, i: Int): Either[ConfigError, Int] = 82 | if (i >= 0) Either.right(i) 83 | else Either.left(ParseError(field)) 84 | } 85 | -------------------------------------------------------------------------------- /src/test/scala/catslib/ApplicativeSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class ApplicativeSpec extends RefSpec with Checkers { 26 | def `pure method` = { 27 | check( 28 | Test.testSuccess( 29 | ApplicativeSection.pureMethod _, 30 | Option(1) :: List(1) :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `applicative composition` = { 36 | check( 37 | Test.testSuccess( 38 | ApplicativeSection.applicativeComposition _, 39 | List(Option(1)) :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `applicative and monad` = { 45 | check( 46 | Test.testSuccess( 47 | ApplicativeSection.applicativesAndMonads _, 48 | Option(1) :: Option(1) :: HNil 49 | ) 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/scala/catslib/ApplySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class ApplySpec extends RefSpec with Checkers { 26 | def `extends functor` = { 27 | val theNone: Option[Int] = None 28 | 29 | check( 30 | Test.testSuccess( 31 | ApplySection.applyExtendsFunctor _, 32 | Option("1") :: Option(2) :: theNone :: HNil 33 | ) 34 | ) 35 | } 36 | 37 | def `is composable` = { 38 | val result: List[Option[Int]] = List(Some(2), None, Some(4)) 39 | 40 | check( 41 | Test.testSuccess( 42 | ApplySection.applyComposes _, 43 | result :: HNil 44 | ) 45 | ) 46 | } 47 | 48 | def `ap method` = { 49 | val theNone: Option[Int] = None 50 | 51 | check( 52 | Test.testSuccess( 53 | ApplySection.applyAp _, 54 | Option("1") :: Option(2) :: theNone :: theNone :: theNone :: HNil 55 | ) 56 | ) 57 | } 58 | 59 | def `apN method` = { 60 | val theNone: Option[Int] = None 61 | 62 | check( 63 | Test.testSuccess( 64 | ApplySection.applyApn _, 65 | Option(3) :: theNone :: Option(6) :: HNil 66 | ) 67 | ) 68 | } 69 | 70 | def `mapN method` = { 71 | check( 72 | Test.testSuccess( 73 | ApplySection.applyMapn _, 74 | Option(3) :: Option(6) :: HNil 75 | ) 76 | ) 77 | } 78 | 79 | def `tupleN method` = { 80 | check( 81 | Test.testSuccess( 82 | ApplySection.applyTuplen _, 83 | Option((1, 2)) :: Option((1, 2, 3)) :: HNil 84 | ) 85 | ) 86 | } 87 | 88 | def `builder syntax` = { 89 | val aNone: Option[Int] = None 90 | val anotherNone: Option[(Int, Int, Int)] = None 91 | 92 | check( 93 | Test.testSuccess( 94 | ApplySection.applyBuilderSyntax _, 95 | Option(3) :: aNone :: Option(3) :: aNone :: Option((1, 2)) :: anotherNone :: HNil 96 | ) 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/scala/catslib/EitherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import cats.implicits._ 20 | import org.scalacheck.ScalacheckShapeless._ 21 | import org.scalaexercises.Test 22 | import org.scalatestplus.scalacheck.Checkers 23 | import org.scalatest.refspec.RefSpec 24 | import shapeless.HNil 25 | 26 | class EitherSpec extends RefSpec with Checkers { 27 | def `is right biased` = { 28 | check( 29 | Test.testSuccess( 30 | EitherSection.eitherMapRightBias _, 31 | Either.right[String, Int](6) :: Either.left[String, Int]("Something went wrong") :: HNil 32 | ) 33 | ) 34 | } 35 | 36 | def `has a Monad implementation` = { 37 | check( 38 | Test.testSuccess( 39 | EitherSection.eitherMonad _, 40 | Either.right[String, Int](6) :: Either.left[String, Int]("Something went wrong") :: HNil 41 | ) 42 | ) 43 | } 44 | 45 | def `Either instead of exceptions` = { 46 | check( 47 | Test.testSuccess( 48 | EitherSection.eitherStyleParse _, 49 | false :: true :: HNil 50 | ) 51 | ) 52 | } 53 | 54 | def `Either composes nicely` = { 55 | check( 56 | Test.testSuccess( 57 | EitherSection.eitherComposition _, 58 | false :: true :: false :: HNil 59 | ) 60 | ) 61 | } 62 | 63 | def `Either can carry exceptions on the left` = { 64 | check( 65 | Test.testSuccess( 66 | EitherSection.eitherExceptions _, 67 | "Got reciprocal: 0.5" :: HNil 68 | ) 69 | ) 70 | } 71 | 72 | def `Either can carry a value of an error ADT on the left` = { 73 | check( 74 | Test.testSuccess( 75 | EitherSection.eitherErrorsAsAdts _, 76 | "Got reciprocal: 0.5" :: HNil 77 | ) 78 | ) 79 | } 80 | 81 | def `Either in the large` = { 82 | check( 83 | Test.testSuccess( 84 | EitherSection.eitherInTheLarge _, 85 | Either.right[String, Int](42) :: Either 86 | .left[String, Int]("Hello") :: Either.left[String, Int]("olleH") :: HNil 87 | ) 88 | ) 89 | } 90 | 91 | def `Either with exceptions` = { 92 | check( 93 | Test.testSuccess( 94 | EitherSection.eitherWithExceptions _, 95 | false :: true :: HNil 96 | ) 97 | ) 98 | } 99 | 100 | def `Either syntax` = { 101 | check( 102 | Test.testSuccess( 103 | EitherSection.eitherSyntax _, 104 | Either.right[String, Int](42) :: HNil 105 | ) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/scala/catslib/EvalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatest.refspec.RefSpec 22 | import org.scalatestplus.scalacheck.Checkers 23 | import shapeless.HNil 24 | 25 | class EvalSpec extends RefSpec with Checkers { 26 | 27 | def `eager eval (now) is evaluated` = { 28 | check( 29 | Test.testSuccess( 30 | EvalSection.nowEval _, 31 | List(1, 2, 3) :: HNil 32 | ) 33 | ) 34 | } 35 | 36 | def `later eval is only evaluated` = { 37 | check( 38 | Test.testSuccess( 39 | EvalSection.laterEval _, 40 | List(1, 2) :: 1 :: HNil 41 | ) 42 | ) 43 | } 44 | 45 | def `always eval is only evaluated` = { 46 | check( 47 | Test.testSuccess( 48 | EvalSection.alwaysEval _, 49 | 4 :: List(1, 2, 3, 4) :: 5 :: HNil 50 | ) 51 | ) 52 | } 53 | 54 | def `defer eval chaining with flatMap` = { 55 | check( 56 | Test.testSuccess( 57 | EvalSection.deferEval _, 58 | List(0, 0, 0) :: HNil 59 | ) 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/scala/catslib/FoldableSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class FoldableSpec extends RefSpec with Checkers { 26 | def `foldLeft function` = { 27 | check( 28 | Test.testSuccess( 29 | FoldableSection.foldableFoldLeft _, 30 | 6 :: "abc" :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `foldRight function` = { 36 | check( 37 | Test.testSuccess( 38 | FoldableSection.foldableFoldRight _, 39 | 6 :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `fold function` = { 45 | check( 46 | Test.testSuccess( 47 | FoldableSection.foldableFold _, 48 | "abc" :: 6 :: HNil 49 | ) 50 | ) 51 | } 52 | 53 | def `foldMap function` = { 54 | check( 55 | Test.testSuccess( 56 | FoldableSection.foldableFoldMap _, 57 | 3 :: "123" :: HNil 58 | ) 59 | ) 60 | } 61 | 62 | def `find function` = { 63 | val aNone: Option[Int] = None 64 | 65 | check( 66 | Test.testSuccess( 67 | FoldableSection.foldableFind _, 68 | Option(3) :: aNone :: HNil 69 | ) 70 | ) 71 | } 72 | 73 | def `exists function` = { 74 | check( 75 | Test.testSuccess( 76 | FoldableSection.foldableExists _, 77 | true :: false :: HNil 78 | ) 79 | ) 80 | } 81 | 82 | def `forall function` = { 83 | check( 84 | Test.testSuccess( 85 | FoldableSection.foldableForall _, 86 | true :: false :: HNil 87 | ) 88 | ) 89 | } 90 | 91 | def `foldK function` = { 92 | val aNone: Option[Unit] = None 93 | 94 | check( 95 | Test.testSuccess( 96 | FoldableSection.foldableFoldk _, 97 | List(1, 2, 3, 4, 5) :: Option("two") :: HNil 98 | ) 99 | ) 100 | } 101 | 102 | def `tolist function` = { 103 | val emptyList: List[Int] = Nil 104 | 105 | check( 106 | Test.testSuccess( 107 | FoldableSection.foldableTolist _, 108 | List(1, 2, 3) :: List(42) :: emptyList :: HNil 109 | ) 110 | ) 111 | } 112 | 113 | def `filter function` = { 114 | val emptyList: List[Int] = Nil 115 | 116 | check( 117 | Test.testSuccess( 118 | FoldableSection.foldableFilter _, 119 | List(1, 2) :: emptyList :: HNil 120 | ) 121 | ) 122 | } 123 | 124 | def `traverse_ function` = { 125 | val aNone: Option[Unit] = None 126 | 127 | check( 128 | Test.testSuccess( 129 | FoldableSection.foldableTraverse _, 130 | Option(()) :: aNone :: HNil 131 | ) 132 | ) 133 | } 134 | 135 | def `compose foldables` = { 136 | check( 137 | Test.testSuccess( 138 | FoldableSection.foldableCompose _, 139 | 10 :: "123" :: HNil 140 | ) 141 | ) 142 | } 143 | 144 | def `foldable methods` = { 145 | check( 146 | Test.testSuccess( 147 | FoldableSection.foldableMethods _, 148 | false :: List(2, 3) :: List(1) :: HNil 149 | ) 150 | ) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/scala/catslib/FunctorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class FunctorSpec extends RefSpec with Checkers { 26 | def `using functor` = { 27 | val theNone: Option[Int] = None 28 | 29 | check( 30 | Test.testSuccess( 31 | FunctorSection.usingFunctor _, 32 | Option(5) :: theNone :: HNil 33 | ) 34 | ) 35 | } 36 | 37 | def `lifting to a functor` = { 38 | check( 39 | Test.testSuccess( 40 | FunctorSection.liftingToAFunctor _, 41 | Option(5) :: HNil 42 | ) 43 | ) 44 | } 45 | 46 | def `using fproduct` = { 47 | check( 48 | Test.testSuccess( 49 | FunctorSection.usingFproduct _, 50 | 4 :: 2 :: 7 :: HNil 51 | ) 52 | ) 53 | } 54 | 55 | def `composing functors` = { 56 | val result: List[Option[Int]] = List(Option(2), None, Option(4)) 57 | 58 | check( 59 | Test.testSuccess( 60 | FunctorSection.composingFunctors _, 61 | result :: HNil 62 | ) 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/scala/catslib/IdentitySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class IdentitySpec extends RefSpec with Checkers { 26 | def `Id type` = { 27 | check( 28 | Test.testSuccess( 29 | IdentitySection.identityType _, 30 | 42 :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `Id has pure` = { 36 | check( 37 | Test.testSuccess( 38 | IdentitySection.pureIdentity _, 39 | 42 :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `Id Comonad` = { 45 | check( 46 | Test.testSuccess( 47 | IdentitySection.idComonad _, 48 | 43 :: HNil 49 | ) 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/scala/catslib/MonadSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class MonadSpec extends RefSpec with Checkers { 26 | def `flatten function` = { 27 | val aNone: Option[Int] = None 28 | 29 | check( 30 | Test.testSuccess( 31 | MonadSection.flattenRecap _, 32 | Option(1) :: aNone :: List(1, 2, 3) :: HNil 33 | ) 34 | ) 35 | } 36 | 37 | def `monad instances` = { 38 | check( 39 | Test.testSuccess( 40 | MonadSection.monadInstances _, 41 | Option(42) :: HNil 42 | ) 43 | ) 44 | } 45 | 46 | def `flatmap function` = { 47 | check( 48 | Test.testSuccess( 49 | MonadSection.monadFlatmap _, 50 | List(1, 1, 2, 2, 3, 3) :: HNil 51 | ) 52 | ) 53 | } 54 | 55 | def `ifM function` = { 56 | check( 57 | Test.testSuccess( 58 | MonadSection.monadIfm _, 59 | Option("truthy") :: List(1, 2, 3, 4, 1, 2) :: List(3, 4, 1, 2, 3, 4) :: HNil 60 | ) 61 | ) 62 | } 63 | 64 | def `monad composition` = { 65 | check( 66 | Test.testSuccess( 67 | MonadSection.monadComposition _, 68 | List(Option(42)) :: HNil 69 | ) 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/catslib/MonoidSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class MonoidSpec extends RefSpec with Checkers { 26 | def `has a empty operation` = { 27 | check( 28 | Test.testSuccess( 29 | MonoidSection.monoidEmpty _, 30 | "" :: "abc" :: "" :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `advantages of using monoid operations` = { 36 | val aMap: Map[String, Int] = Map("a" -> 4, "b" -> 2) 37 | val anotherMap: Map[String, Int] = Map() 38 | 39 | check( 40 | Test.testSuccess( 41 | MonoidSection.monoidAdvantage _, 42 | aMap :: anotherMap :: HNil 43 | ) 44 | ) 45 | } 46 | 47 | def `accumulating with a monoid on foldMap` = { 48 | check( 49 | Test.testSuccess( 50 | MonoidSection.monoidFoldmap _, 51 | 15 :: "12345" :: HNil 52 | ) 53 | ) 54 | } 55 | 56 | def `accumulating with a tuple on foldMap` = { 57 | check( 58 | Test.testSuccess( 59 | MonoidSection.tupleMonoid _, 60 | (15, "12345") :: HNil 61 | ) 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/scala/catslib/OptionTSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatest.refspec.RefSpec 22 | import org.scalatestplus.scalacheck.Checkers 23 | import shapeless.HNil 24 | 25 | class OptionTSpec extends RefSpec with Checkers { 26 | 27 | def `Option pure value` = { 28 | check( 29 | Test.testSuccess( 30 | OptionTSection.pureOption _, 31 | "Hola" :: "Hello" :: "" :: HNil 32 | ) 33 | ) 34 | } 35 | 36 | def `From option` = { 37 | check( 38 | Test.testSuccess( 39 | OptionTSection.fromOptionT _, 40 | Option("Bonjour") :: Option("Ciao") :: Option.empty[String] :: HNil 41 | ) 42 | ) 43 | } 44 | 45 | def `OptionT methods` = { 46 | check( 47 | Test.testSuccess( 48 | OptionTSection.optionTMethods _, 49 | List(Option("Hallo!"), Option("Hi!"), Option("Guten Morgen!"), Option.empty[String]) :: 50 | (List( 51 | Option("Hallo World!"), 52 | Option("Hi World!"), 53 | Option("Guten Morgen World!"), 54 | Option.empty[String] 55 | )) :: 56 | List(Option("Hallo"), Option("Hi"), Option.empty[String], Option.empty[String]) :: 57 | List( 58 | Option.empty[String], 59 | Option.empty[String], 60 | Option("Guten Morgen"), 61 | Option.empty[String] 62 | ) :: 63 | List("Hallo", "Hi", "Guten Morgen", "Guten Tag") :: 64 | List("Hallo", "Hi", "Guten Morgen", "Guten Abend") :: HNil 65 | ) 66 | ) 67 | } 68 | 69 | def `For comprehension` = { 70 | check( 71 | Test.testSuccess( 72 | OptionTSection.forComprehension _, 73 | Option(1.5) :: HNil 74 | ) 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/scala/catslib/SemigroupSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class SemigroupSpec extends RefSpec with Checkers { 26 | def `has a combine operation` = { 27 | check( 28 | Test.testSuccess( 29 | SemigroupSection.semigroupCombine _, 30 | 3 :: List(1, 2, 3, 4, 5, 6) :: Option(3) :: Option(1) :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `has an advanced combine operation` = { 36 | check( 37 | Test.testSuccess( 38 | SemigroupSection.semigroupCombineComplex _, 39 | 67 :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `composes with other semigroups` = { 45 | check( 46 | Test.testSuccess( 47 | SemigroupSection.composingSemigroups _, 48 | Map("bar" -> 11) :: HNil 49 | ) 50 | ) 51 | } 52 | 53 | def `has special syntax` = { 54 | val aNone: Option[Int] = None 55 | 56 | check( 57 | Test.testSuccess( 58 | SemigroupSection.semigroupSpecialSyntax _, 59 | Option(3) :: Option(2) :: aNone :: Option(2) :: HNil 60 | ) 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/scala/catslib/TraverseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class TraverseSpec extends RefSpec with Checkers { 26 | def `traverseu function with either` = { 27 | check( 28 | Test.testSuccess( 29 | TraverseSection.traverseuFunction _, 30 | List(1, 2, 3) :: true :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `traverseu function with validated` = { 36 | check( 37 | Test.testSuccess( 38 | TraverseSection.traverseuValidated _, 39 | true :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `sequencing effects` = { 45 | val aNone: Option[List[Int]] = None 46 | 47 | check( 48 | Test.testSuccess( 49 | TraverseSection.sequencing _, 50 | Option(List(1, 2, 3)) :: aNone :: HNil 51 | ) 52 | ) 53 | } 54 | 55 | def `traversing for effects` = { 56 | val aNone: Option[Unit] = None 57 | 58 | check( 59 | Test.testSuccess( 60 | TraverseSection.traversingForEffects _, 61 | Option(()) :: aNone :: HNil 62 | ) 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/scala/catslib/ValidatedSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2020 47 Degrees Open Source 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 catslib 18 | 19 | import org.scalacheck.ScalacheckShapeless._ 20 | import org.scalaexercises.Test 21 | import org.scalatestplus.scalacheck.Checkers 22 | import org.scalatest.refspec.RefSpec 23 | import shapeless.HNil 24 | 25 | class ValidatedSpec extends RefSpec with Checkers { 26 | def `with no errors` = { 27 | check( 28 | Test.testSuccess( 29 | ValidatedSection.noErrors _, 30 | true :: "127.0.0.1" :: 1337 :: HNil 31 | ) 32 | ) 33 | } 34 | 35 | def `with accumulating errors` = { 36 | check( 37 | Test.testSuccess( 38 | ValidatedSection.someErrors _, 39 | false :: true :: HNil 40 | ) 41 | ) 42 | } 43 | 44 | def `sequential validation` = { 45 | check( 46 | Test.testSuccess( 47 | ValidatedSection.sequentialValidation _, 48 | false :: true :: HNil 49 | ) 50 | ) 51 | } 52 | 53 | def `validation with either` = { 54 | check( 55 | Test.testSuccess( 56 | ValidatedSection.validatedAsEither _, 57 | false :: true :: HNil 58 | ) 59 | ) 60 | } 61 | } 62 | --------------------------------------------------------------------------------