├── project
├── build.properties
└── plugins.sbt
├── version.sbt
├── .gitignore
├── core
└── src
│ ├── main
│ └── scala
│ │ └── io
│ │ └── chrisdavenport
│ │ └── selection
│ │ ├── implicits
│ │ └── implicits.scala
│ │ ├── package.scala
│ │ ├── syntax
│ │ ├── all.scala
│ │ └── selection.scala
│ │ └── Selection.scala
│ └── test
│ └── scala
│ └── io
│ └── chrisdavenport
│ └── selection
│ ├── CompileSpec.scala
│ ├── SelectionTests.scala
│ └── SelectionSpec.scala
├── .mergify.yml
├── NOTICE
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
├── README.md
├── .travis.yml
├── LICENSE
├── licenses
└── LICENSE_selections
└── docs
└── src
└── main
└── tut
├── index.md
└── basic_tutorial.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.6.2
2 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | (ThisBuild / version) := "0.1.1-SNAPSHOT"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea/
3 | # vim
4 | *.sw?
5 |
6 | # Ignore [ce]tags files
7 | tags
8 |
9 | .metals
10 | .bloop
11 |
--------------------------------------------------------------------------------
/core/src/main/scala/io/chrisdavenport/selection/implicits/implicits.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 |
3 | package object implicits extends syntax.all
--------------------------------------------------------------------------------
/core/src/main/scala/io/chrisdavenport/selection/package.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport
2 |
3 | package object selection {
4 | type SelectionA[F[_], A] = Selection[F, A, A]
5 | }
--------------------------------------------------------------------------------
/core/src/main/scala/io/chrisdavenport/selection/syntax/all.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 | package syntax
3 |
4 | trait all extends selection
5 | object all extends all
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatically merge scala-steward's PRs
3 | conditions:
4 | - author=scala-steward
5 | - status-success=Travis CI - Pull Request
6 | - body~=labels:.*semver-patch.*
7 | actions:
8 | merge:
9 | method: merge
10 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | selection
2 | Copyright 2018 Christopher Davenport
3 | Licensed under the MIT License (see LICENSE)
4 |
5 | This software contains portions of code derived from selections
6 | https://github.com/ChrisPenner/selections
7 | Copyright Chris Penner (c) 2017
8 | Licensed under BSD3 (see licenses/LICENSE_selections)
--------------------------------------------------------------------------------
/core/src/main/scala/io/chrisdavenport/selection/syntax/selection.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 | package syntax
3 |
4 | import cats._
5 |
6 | trait selection {
7 | implicit class selectionCreationFunctorOps[F[_]: Functor, A](private val fa: F[A]){
8 | def newSelection: Selection[F, A, A] = Selection.newSelection(fa)
9 | def newSelectionB[B]: Selection[F, B, A] = Selection.newSelectionB(fa)
10 | }
11 | }
12 |
13 | object selection extends selection
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other such characteristics.
4 |
5 | Everyone is expected to follow the [Scala Code of Conduct] when discussing the project on the available communication channels. If you are being harassed, please contact us immediately so that we can support you.
6 |
7 | ## Moderation
8 |
9 | Any questions, concerns, or moderation requests please contact a member of the project.
10 |
11 | - [Christopher Davenport](mailto:chris@christopherdavenport.tech)
12 |
13 | [Scala Code of Conduct]: https://www.scala-lang.org/conduct/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # changelog
2 |
3 | This file summarizes **notable** changes for each release, but does not describe internal changes unless they are particularly exciting. This change log is ordered chronologically, so each release contains all changes described below it.
4 |
5 | ----
6 |
7 | ## Unreleased Changes
8 |
9 | - Add mapExclude and collectExclude combinators [#25](https://github.com/ChristopherDavenport/selection/pull/25)
10 |
11 | ## New and Noteworthy for Version 0.1.0
12 |
13 | Selections for all! This is the first stable release for selection. In this release we are generating the initial selection class and operators, constructors, typeclass instances. Property tested and fully law checked. [Cats](https://github.com/typelevel/cats) is the only dependency and this version is based off 1.4.0.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # selection [](https://travis-ci.com/ChristopherDavenport/selection) [](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/selection_2.12)
2 |
3 | selection is a Scala library for transforming subsets of values within a functor. Inspired by [selections](https://github.com/ChrisPenner/selections)
4 |
5 | ## [Head on over to the microsite](https://davenverse.github.io/selection/)
6 |
7 | ## Quick Start
8 |
9 | To use selection in an existing SBT project with Scala 2.11 or a later version, add the following dependencies to your
10 | `build.sbt` depending on your needs:
11 |
12 | ```scala
13 | libraryDependencies ++= Seq(
14 | "io.chrisdavenport" %% "selection" % ""
15 | )
16 | ```
17 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.dwijnand" % "sbt-travisci" % "1.2.0")
2 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0")
3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2")
4 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.22")
5 | addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.1.2")
6 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13")
7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.12")
8 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.6.4")
9 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.6.13")
10 | addSbtPlugin("com.47deg" % "sbt-microsites" % "0.9.4")
11 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3")
12 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
13 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.5.1")
14 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0")
15 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0")
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: scala
3 |
4 | scala:
5 | - 2.12.8
6 | - 2.11.12
7 |
8 | jdk:
9 | - openjdk8
10 |
11 | before_install:
12 | - export PATH=${PATH}:./vendor/bundle
13 |
14 | install:
15 | - rvm use 2.6.0 --install --fuzzy
16 | - gem update --system
17 | - gem install sass
18 | - gem install jekyll -v 3.2.1
19 |
20 | script:
21 | - sbt ++$TRAVIS_SCALA_VERSION test
22 | - sbt ++$TRAVIS_SCALA_VERSION mimaReportBinaryIssues
23 | - sbt ++$TRAVIS_SCALA_VERSION docs/makeMicrosite
24 |
25 | after_success:
26 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && test $TRAVIS_REPO_SLUG == "ChristopherDavenport/selection" && sbt ++$TRAVIS_SCALA_VERSION publish
27 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && test $TRAVIS_REPO_SLUG == "ChristopherDavenport/selection" && test $TRAVIS_SCALA_VERSION == "2.12.7" && sbt docs/publishMicrosite
28 |
29 | cache:
30 | directories:
31 | - $HOME/.ivy2/cache
32 | - $HOME/.coursier/cache
33 | - $HOME/.sbt
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Christopher Davenport
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/core/src/test/scala/io/chrisdavenport/selection/CompileSpec.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 |
3 | import cats._
4 | import cats.implicits._
5 |
6 | object CompileSpec {
7 | def mapSelection[F[_]: Functor, A, B](fa: Selection[F, A, A])(f: A => B): Selection[F, A, B] =
8 | fa.map(f)
9 |
10 | def flatMapSelection[F[_]: Monad, A, B](fa: Selection[F, A, A])(f: A => Selection[F, A, B]): Selection[F, A, B] =
11 | fa.flatMap(f)
12 |
13 | def bifoldableSelection[F[_]: Foldable, A, B, C](fa: Selection[F, A, B])(c: C,f: (C, A) => C,g: (C, B) => C): C =
14 | fa.bifoldLeft(c)(f, g)
15 |
16 | def bitraverseSelection[F[_]: Traverse, G[_]:Applicative, A, B, C, D](
17 | fab: Selection[F,A,B])(f: A => G[C], g: B => G[D]): G[Selection[F, C, D]] =
18 | fab.bitraverse(f, g)
19 |
20 | def foldableSelection[F[_]: Foldable, A, B, C](fa: Selection[F,C,A],b: B)(f: (B, A) => B): B =
21 | fa.foldLeft(b)(f)
22 |
23 | def traverseSelection[F[_]: Traverse, G[_]: Applicative, C, A, B](
24 | fa: Selection[F,C,A])(f: A => G[B]): G[Selection[F,C,B]] = fa.traverse(f)
25 |
26 | def bifunctorSelection[F[_]: Functor, A, B, C, D](fab: Selection[F,A,B])(f: A => C, g: B => D): Selection[F,C,D] =
27 | fab.bimap(f, g)
28 |
29 | def showSelection[F[_], B,A ](fa: Selection[F, B,A])(implicit show: Show[F[Either[B, A]]]): String =
30 | fa.show
31 |
32 | def eqSelection[F[_], B, A](sa: Selection[F, B, A], sb: Selection[F, B, A])(implicit eq: Eq[F[Either[B, A]]]): Boolean =
33 | sa === sb
34 | }
--------------------------------------------------------------------------------
/core/src/test/scala/io/chrisdavenport/selection/SelectionTests.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 |
3 | import cats._
4 | // import cats.data._
5 | // import cats.implicits._
6 | // import org.scalacheck.cats._
7 | import cats.tests.CatsSuite
8 | import cats.kernel.laws.discipline._
9 | import cats.laws.discipline._
10 | import org.scalacheck._
11 |
12 |
13 | class SelectionTests extends CatsSuite {
14 | implicit def arbSelection[F[_]: Functor, E, A](implicit A: Arbitrary[F[A]]): Arbitrary[Selection[F, E, A]] =
15 | Arbitrary{A.arbitrary.map(Selection.newSelectionB[F, E, A](_))}
16 | implicit def arbFunction[B, A: Arbitrary]: Arbitrary[B => A] = Arbitrary{
17 | for {
18 | a <- Arbitrary.arbitrary[A]
19 | } yield {_: B => a}
20 | }
21 |
22 | // *
23 | checkAll("Selection", EqTests[Selection[List, Int, Int]].eqv)
24 |
25 | // * -> *
26 | checkAll("Selection", FoldableTests[Selection[List, Int, ?]].foldable[Int, Int])
27 | checkAll("Selection", FunctorTests[Selection[List, Int, ?]].functor[Int, Int, Int])
28 | checkAll("Selection", TraverseTests[Selection[List, Int, ?]].traverse[Int, Int, Int, Int, Id, Id])
29 | checkAll("Selection", MonadTests[Selection[List, Int, ?]].monad[Int, Int, Int])
30 |
31 | // * -> * -> *
32 | checkAll("Selection", BifoldableTests[Selection[List, ?, ?]].bifoldable[Int, Int, Int])
33 | checkAll("Selection", BifunctorTests[Selection[List, ?, ?]].bifunctor[Int, Int, Int, Int, Int, Int])
34 | checkAll("Selection", BitraverseTests[Selection[List, ?, ?]].bitraverse[Id, Int, Int, Int, Int, Int, Int])
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/licenses/LICENSE_selections:
--------------------------------------------------------------------------------
1 | Copyright Chris Penner (c) 2017
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials provided
14 | with the distribution.
15 |
16 | * Neither the name of Chris Penner nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/core/src/test/scala/io/chrisdavenport/selection/SelectionSpec.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 |
3 | import org.specs2._
4 | import cats.implicits._
5 | import implicits._
6 |
7 | object SelectionSpec extends mutable.Specification with ScalaCheck {
8 |
9 | "Selection" should {
10 | "return original functor after creation and forget" >> prop { l: List[Int] =>
11 | l.newSelection.forgetSelection must_=== l
12 | }
13 | "return original functor after creation and getSelected" >> prop { l: List[Int] =>
14 | l.newSelection.getSelected must_=== l
15 | }
16 |
17 | "return no values unselected of a newly created selectin" >> prop { l: List[Int] =>
18 | l.newSelection.getUnselected must_=== List.empty[Int]
19 | }
20 | "return all values unselected after inversion" >> prop { l: List[Int] =>
21 | l.newSelection.invertSelection.getUnselected must_=== l
22 | }
23 | "deselectAll must exclude all values" >> prop { l: List[Int] =>
24 | l.newSelection.deselectAll.getSelected must_=== List.empty[Int]
25 | }
26 |
27 | "selectAll must include all values" >> prop {l : List[Int] =>
28 | l.newSelection.invertSelection.selectAll.getSelected must_=== l
29 | }
30 |
31 | "exclude must exclude values on a predicate" >> prop {l : List[Int] =>
32 | l.newSelection.exclude(_ < 100).getUnselected.forall(_ < 100) must_=== true
33 | }
34 | "include must include values on a predicate" >> prop {l: List[Int] =>
35 | l.newSelection.invertSelection.include(_ < 100).getSelected.forall(_ < 100) must_=== true
36 | }
37 |
38 | "return only values matching a predicate with select" >> prop { l: List[Int] =>
39 | l.newSelection.select(_ > 100).getSelected must_=== l.filter(_ > 100)
40 | }
41 | "selected must be empty if mapExclude is None" >> prop { l: List[Int] =>
42 | l.newSelection.mapExclude(_ => None).getSelected must_=== List.empty
43 | }
44 | "excluded must be all values if mapExclude is None" >> prop { l: List[Int] =>
45 | l.newSelection.mapExclude(_ => None).getUnselected must_=== l
46 | }
47 | "selected must be all values if mapExclude is pure" >> prop {l: List[Int] =>
48 | l.newSelection.mapExclude(_.pure[Option]).getSelected must_=== l
49 | }
50 | "collect must only collect values matching the partial" >> prop {l: List[Option[Int]] =>
51 | l.newSelection.collectExclude{ case Some(i) => i}.getSelected must_=== l.flattenOption
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/docs/src/main/tut/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | ---
5 | # selection [](https://travis-ci.com/ChristopherDavenport/selection) [](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/selection_2.12)
6 |
7 | selection is a Scala library for transforming subsets of values within a functor. Inspired by [selections](https://github.com/ChrisPenner/selections)
8 |
9 | Ever wished you could select just a few values within a functor, perform some operations on them, then flatten them back into the plain old functor again? Now you can!
10 |
11 | Selection is a wrapper around Functors which adds several combinators and interesting instances. Wrapping a functor in Selection allows you to:
12 |
13 | - Select specific values within your functor according to a predicate
14 | - Expand/Contract a selection based on additional predicates using include and exclude
15 | - Select values based on their context if your functor is also a Comonad
16 | - Map over unselected and/or selected values using Bifunctor
17 | - Traverse over unselected and/or selected values using Bitraversable
18 | - Fold over unselected and/or selected values using Bifoldable
19 | - Perform monad computations over selected values if your functor is a Monad
20 | - Extract all unselected or selected elements to a list
21 | - Deselect and return to your original functor using unify
22 |
23 | ## When Should/Shouldn't I Use Selection?
24 |
25 | You can use selection whenever you've got a bunch of things and you want to operate over just a few of them at a time. You can do everything that selection provides by combining a bunch of predicates with map, but it gets messy really quick; selection provides a clean interface for this sort of operation.
26 |
27 | You shouldn't use selections when you're looking for a monadic interface, selections works at the value level typically chaining commands together, while it can be used as a monad transformer if the underlying functor is also a monad, however at that point you may be better served using [EitherT](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/data/EitherT.scala)
28 |
29 | ## Quick Start
30 |
31 | To use selection in an existing SBT project with Scala 2.11 or a later version, add the following dependencies to your
32 | `build.sbt` depending on your needs:
33 |
34 | ```scala
35 | libraryDependencies ++= Seq(
36 | "io.chrisdavenport" %% "selection" % ""
37 | )
38 | ```
39 |
40 | ## Quick Example
41 |
42 | First Imports.
43 |
44 | ```tut:silent
45 | import cats.implicits._ // For Syntax Enhancements
46 | import io.chrisdavenport.selection._ // Selection Type
47 | import io.chrisdavenport.selection.implicits._ // Implicit Syntax On Functors
48 | ```
49 |
50 | Here's how it looks.
51 |
52 | ```tut:book
53 | val xs = List(1,2,3,4,5,6)
54 |
55 | {
56 | xs.newSelection
57 | .select(_ % 2 === 0)
58 | .mapSelected(_ + 100)
59 | .bimap(odd => show"Odd: $odd", even => show"Even: $even")
60 | .forgetSelection
61 | }
62 |
63 | {
64 | Selection.newSelection(xs)
65 | .select(_ > 3)
66 | .mapSelected(_ + 10)
67 | .exclude(_ < 15)
68 | .mapSelected(_ + 10)
69 | .forgetSelection
70 | }
71 | ```
--------------------------------------------------------------------------------
/docs/src/main/tut/basic_tutorial.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: "tutorial"
4 | section: "tutorial"
5 | position: 1
6 | ---
7 |
8 | To start the process lets get our imports out of the way.
9 |
10 | ```tut:silent
11 | import cats._
12 | import cats.implicits._
13 | import cats.derived._ // For Kittens Derivation
14 | import io.chrisdavenport.selection._
15 | import io.chrisdavenport.selection.implicits._
16 | import cats.effect._ // For Effect Display
17 | ```
18 |
19 | Now let's build some data
20 |
21 | ```tut:book
22 | // Weirdness in this block is for companion object behavior in tut
23 |
24 | sealed trait Country; case object USA extends Country; case object Canada extends Country; object Country {
25 | implicit val showCountry: Show[Country] = semi.show
26 | implicit val eqCountry: Eq[Country] = semi.eq
27 | }
28 |
29 | final case class Account(name: String, country: Country, balance: Double); object Account {
30 | implicit val showCountry: Show[Account] = semi.show
31 | implicit val eqCountry: Eq[Account] = semi.eq
32 | }
33 | ```
34 |
35 | So the accounts we are modeling are fairly simple. They have a name, country of origin and account balance. Let's make a 'database' of accounts as a simple list:
36 |
37 | ```tut:book
38 | val accounts: List[Account] = List(
39 | Account("Steve" , Canada , 34D),
40 | Account("Cindy" , USA , 10D),
41 | Account("Blake" , USA , -6D),
42 | Account("Carl" , Canada , -16D)
43 | )
44 | ```
45 |
46 | Great! So far so good. Now we see where selections come in handy, let's say we want to accumulate interest for all accounts with a POSITIVE account balance. Normally we'd need to map over every user, check their account balance, then perform the interest calculation. This code is pretty straightforward to write, but it gets a bit clunky in more complex situations. With selections we can select the accounts we want to work with, then map over them specifically!
47 |
48 | One additional complication! USA and Canadian accounts get different interest rates! No problem though, let's see what we can do!
49 |
50 | ```tut:book
51 | type Rate = Double
52 |
53 | def addInterest(rate: Rate)(user: Account): Account = user.copy(balance = user.balance * rate)
54 |
55 | val usaRate = 1.25D
56 | val canRate = 1.10D
57 |
58 | val adjusted : List[Account] = {
59 | accounts
60 | .newSelection
61 | .select(_.country === USA)
62 | .exclude(_.balance < 0)
63 | .mapSelected(addInterest(usaRate)(_))
64 | .select(_.country === Canada)
65 | .exclude(_.balance < 0)
66 | .mapSelected(addInterest(canRate)(_))
67 | .forgetSelection
68 | }
69 | ```
70 |
71 | You can see it made the proper adjustments without touching the negative accounts! Since, quite a bit just happened, lets break it down a bit.
72 |
73 | ```tut:book
74 | val americans : SelectionA[List, Account] = {
75 | accounts.newSelection.select(_.country === USA)
76 | }
77 | ```
78 |
79 | So our first step is to create a selection around the list of users. `newSelection` wraps any `Functor` in a `Selection` type, either through the implicits as shown, or `Selection.newSelection`. The Selection type is `Selection[F[_], B, A]` where `F` is the functor, `B` represents the type of unselected data, `A` represents the type of selected data. These types can diverge as you like, but in this case they are both `Account` so we use the `SelectionA` type alias, which is simply a Selection where both unselected and selected data types are the same.
80 |
81 | Now that we have a new selection we have to tell it what we would like to select! `newSelection` selects all elements by default, which isn't entirely useful, so we use `select` with a predicate to determine which elements we want to be in focus. In this case `.select(_.country === USA)` clears the previous selection, then selects only the american accounts
82 |
83 | As we can see above, we can see that the accounts in the USA are all wrapped in `Right` whereas the others are wrapped in `Left`. As users of the library, you don't need to worry about that, the interface manages those details for you, but seeing it working is cool!
84 |
85 | If we wanted to select the Canadians we could write a predicate for that, or since we know that we are tracking only 2 countries right now, we could use `invertSelection` to flip the selection so that Canadians are focused and Americans are unselected.
86 |
87 | We can now use `getSelected` and `getUnselected` to get a list of USA or Canadian accounts respectively, not how the `Right`'s and `Left`'s disappear whenever we stop working within a Selection:
88 |
89 | ```tut:book
90 | americans.getSelected
91 |
92 | americans.getUnselected
93 | ```
94 |
95 | We've got our americans selected, but Blake has a negative account balance! Let's `exclude` any accounts with a negative balance. `exclude` keeps the current selection, but removes any elements that fail the predicate. There's also an `include` combinator which will add any unselected elements which do pass the predicate(assuming you want that).
96 |
97 | ```tut:book
98 | val americansPositive = americans.exclude(_.balance < 0)
99 | ```
100 |
101 | Now we can finally make our transformation, `mapSelected` is provided if you want a nicely named combinator, however its just a synonym for `map`.
102 |
103 | ```tut:book
104 | val americansAdjusted = americansPositive.mapSelected(addInterest(usaRate)(_))
105 | ```
106 |
107 | All selections are `Bifunctors`, so you can `bimap` over the unselected and selected values respectively if you like. Let's say there was a banking error in our user's favour (it happens all the time I swear). All Americans get a $10 credit, all Canadians get a $7 credit!
108 |
109 | ```tut:book
110 | def adjustBalance(adjustment: Double => Double)(account: Account): Account = {
111 | account.copy(balance = adjustment(account.balance))
112 | }
113 |
114 | val withCredit : List[Account] = {
115 | accounts
116 | .newSelection
117 | .select(_.country === USA)
118 | .bimap(adjustBalance(_ + 7D)(_), adjustBalance(_ + 10D)(_))
119 | .forgetSelection
120 | }
121 | ```
122 |
123 | They're also Bitraversable and Bifoldable, so we can perform operations with effects over each segement independently or
124 | perform different effectful operations over each type. Let's print out a warning to all users with a negative balance!
125 |
126 | ```tut:book
127 |
128 | val warnDelinquents = {
129 | def warn(user: Account): IO[Unit] = IO(println(user.name ++ ": get your act together!"))
130 | def congrats(user: Account): IO[Unit] = IO(println(user.name ++ " you're doing great!"))
131 |
132 | accounts
133 | .newSelection
134 | .select(_.balance < 0)
135 | .bitraverse(congrats, warn)
136 | .void
137 | }
138 |
139 | warnDelinquents.unsafeRunSync
140 | ```
141 |
142 | You can use the Bifoldable instance to do similarly interesting things, getSelected and getUnselected are provided as helpers which return lists of the selected and unselected items.
143 |
144 | That's it for this tutorial!
145 |
--------------------------------------------------------------------------------
/core/src/main/scala/io/chrisdavenport/selection/Selection.scala:
--------------------------------------------------------------------------------
1 | package io.chrisdavenport.selection
2 |
3 | import cats._
4 | import cats.implicits._
5 |
6 | /**
7 | * A selection wraps a Functor f and has an unselected type b and a selected type a
8 | *
9 | */
10 | final case class Selection[F[_], B, A](unwrap: F[Either[B, A]]) extends AnyVal {
11 |
12 | /**
13 | * Modify the underlying representation of a selection
14 | */
15 | def modifySelection[G[_], C, D](f:F[Either[B, A]] => G[Either[C, D]]): Selection[G, C, D] = Selection(f(unwrap))
16 |
17 |
18 | /**
19 | * Flip the selection, all selected are now unselected and vice versa
20 | */
21 | def invertSelection(implicit F: Functor[F]): Selection[F, A, B] =
22 | modifySelection(_.map(switch))
23 |
24 | /**
25 | * Map over selected values.
26 | */
27 | def mapSelected[C](f: A => C)(implicit F: Functor[F]): Selection[F, B, C] =
28 | Selection(unwrap.map(_.map(f)))
29 |
30 | /**
31 | * Map over unselected values.
32 | */
33 | def mapUnselected[C](f: B => C)(implicit F: Functor[F]): Selection[F, C, A] =
34 | Selection(unwrap.map(_.leftMap(f)))
35 |
36 | /**
37 | * Collect all selected values into a list. For more complex operations use
38 | * foldMap.
39 | */
40 | def getSelected(implicit F: Foldable[F]): List[A] =
41 | unwrap.foldMap(_.fold(_ => List.empty, List(_)))
42 |
43 | /**
44 | * Collect all unselected values into a list. For more complex operations use
45 | * foldMap.
46 | */
47 | def getUnselected(implicit F: Foldable[F]): List[B] =
48 | unwrap.foldMap(_.fold(List(_), _ => List.empty))
49 |
50 | /**
51 | * Unify selected and unselected and forget the selection
52 | */
53 | def unify[C](f1: B => C)(f2: A => C)(implicit F: Functor[F]): F[C] =
54 | unwrap.map(_.fold(f1, f2))
55 |
56 | /**
57 | * Perform a natural transformation over the underlying container of a selectable
58 | */
59 | def mapK[G[_]](f: F ~> G): Selection[G, B, A] =
60 | Selection(f(unwrap))
61 |
62 | /**
63 | * Exclude Values Not Present in the codomain
64 | */
65 | def mapExclude[C](f: A => Option[C])(implicit F: Functor[F], ev: A =:= B): Selection[F, B, C] =
66 | modifySelection(_.map(_.flatMap(a => f(a).fold(Either.left[B, C](ev(a)))(Either.right))))
67 |
68 | /**
69 | * Similar to mapExclude but a partial Function
70 | */
71 | def collectExclude[C](f: PartialFunction[A, C])(implicit F: Functor[F], ev: A =:= B): Selection[F, B, C] =
72 | mapExclude(f.lift)
73 |
74 | /**
75 | * Drops selection from your functor returning all values (selected or not).
76 | */
77 | def forgetSelection(implicit F: Functor[F], ev: B =:= A): F[A] =
78 | unify(ev)(identity)
79 |
80 | /**
81 | * Add items which match a predicate to the current selection
82 | */
83 | def include(f: A => Boolean)(implicit F: Functor[F], ev: B =:= A): Selection[F,A, A] =
84 | modifySelection(_.map(_.fold[Either[A,A]](b => choose(f)(ev(b)), Either.right)))
85 |
86 | /**
87 | * Remove items which match a predicate to the current selection
88 | */
89 | def exclude(f: A => Boolean)(implicit F: Functor[F], ev: B =:= A): Selection[F, A, A] =
90 | modifySelection(_.map(_.fold(b => Either.left(ev(b)), a => switch(choose(f)(a)))))
91 |
92 | /**
93 | * Select all items in the container
94 | */
95 | def selectAll(implicit F: Functor[F], ev: B =:= A): Selection[F, A, A] =
96 | include(_ => true)
97 |
98 | /**
99 | * Deselect all items in the container
100 | */
101 | def deselectAll(implicit F: Functor[F], ev: B =:= A): Selection[F, A, A]=
102 | exclude(_ => true)
103 |
104 | /**
105 | * Clear the selection then select only items which match a predicate.
106 | */
107 | def select(f: A => Boolean)(implicit F: Functor[F], ev: B =:= A): Selection[F, A, A] =
108 | deselectAll.include(f)
109 |
110 | /**
111 | * Select values based on their context within a comonad.
112 | */
113 | def selectWithContext(f: F[A] => Boolean)(implicit F: Comonad[F], ev: B =:= A): Selection[F, A, A] =
114 | modifySelection{w: F[Either[B, A]] =>
115 | val wa: F[A] = w.map(_.fold(ev, identity))
116 | def waB(w: F[A]): Either[A, A] = choose1[F[A], A](_.extract)(f)(w)
117 | wa.coflatten.map(waB)
118 | }
119 |
120 | // Helpers
121 | private def choose1[C, D](f: C => D)(p: C => Boolean)(a: C) : Either[D, D] =
122 | if (p(a)) Either.right(f(a))
123 | else Either.left(f(a))
124 |
125 | private def choose[C](p: C => Boolean)(a: C): Either[C, C] =
126 | choose1[C, C](identity)(p)(a)
127 |
128 | private def switch[C, D](e: Either[C, D]): Either[D, C] =
129 | e.fold(Either.right, Either.left)
130 |
131 | }
132 |
133 | /**
134 | * Selection Companion Object Holds the constructor methods
135 | * and typeclass instances.
136 | */
137 | object Selection extends SelectionInstances {
138 |
139 | // Constructor
140 | /**
141 | * Create a selection from a functor by selecting all values
142 | */
143 | def newSelection[F[_]: Functor, A](f: F[A]): Selection[F, A, A] =
144 | newSelectionB[F, A, A](f)
145 |
146 | /**
147 | * Create a selection from a functor by selecting all values,
148 | * demands specification of the unselected type.
149 | */
150 | def newSelectionB[F[_]: Functor, B, A](f: F[A]): Selection[F, B, A] =
151 | Selection(f.map(Either.right))
152 |
153 | }
154 |
155 | // Instance Hierarchy
156 | abstract private[selection] class SelectionInstances extends SelectionInstances1 {
157 | implicit def eqSelection[F[_], B, A](implicit eq: Eq[F[Either[B, A]]]): Eq[Selection[F, B, A]] =
158 | Eq.by(_.unwrap)
159 |
160 | implicit def showSelection[F[_], B,A ](implicit show: Show[F[Either[B, A]]]): Show[Selection[F, B, A]] =
161 | Show.show(s =>
162 | s"Selection(${show.show(s.unwrap)})"
163 | )
164 |
165 | implicit def functorBifunctorSelection[F[_]: Functor]: Bifunctor[Selection[F, ?,? ]] =
166 | new Bifunctor[Selection[F, ?,?]]{
167 | def bimap[A, B, C, D](fab: Selection[F,A,B])(f: A => C, g: B => D): Selection[F,C,D] =
168 | Selection(fab.unwrap.map(_.fold(f(_).asLeft, g(_).asRight)))
169 | }
170 |
171 | implicit def traversableSelection[F[_]: Traverse, C]: Traverse[Selection[F, C, ?]] =
172 | new Traverse[Selection[F, C, ?]]{
173 | def foldLeft[A, B](fa: Selection[F,C,A],b: B)(f: (B, A) => B): B =
174 | fa.unwrap.foldLeft(b){
175 | case (b, Right(a)) => f(b, a)
176 | case (b, _) => b
177 | }
178 | def foldRight[A, B](fa: Selection[F,C,A],lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
179 | fa.unwrap.foldRight(lb){
180 | case (Right(a), eb) => f(a, eb)
181 | case (_ , eb) => eb
182 | }
183 | def traverse[G[_]: Applicative, A, B](fa: Selection[F,C,A])(f: A => G[B]): G[Selection[F,C,B]] =
184 | fa.unwrap.traverse(_.traverse(f)).map(Selection(_))
185 | }
186 |
187 | }
188 |
189 | abstract private[selection] class SelectionInstances1 extends SelectionInstances2 {
190 | implicit def foldableSelection[F[_]: Foldable, C]: Foldable[Selection[F, C,?]] =
191 | new Foldable[Selection[F, C, ?]]{
192 | def foldLeft[A, B](fa: Selection[F,C,A],b: B)(f: (B, A) => B): B =
193 | fa.unwrap.foldLeft(b){
194 | case (b, Right(a)) => f(b, a)
195 | case (b, _) => b
196 | }
197 | def foldRight[A, B](fa: Selection[F,C,A],lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
198 | fa.unwrap.foldRight(lb){
199 | case (Right(a), eb) => f(a, eb)
200 | case (_ , eb) => eb
201 | }
202 | }
203 |
204 | implicit def traversableBiTraverseSelection[F[_]: Traverse]: Bitraverse[Selection[F, ?,? ]] =
205 | new Bitraverse[Selection[F, ?, ?]]{
206 | def bifoldLeft[A, B, C](fab: Selection[F,A,B],c: C)(f: (C, A) => C,g: (C, B) => C): C =
207 | fab.unwrap.foldLeft(c){
208 | case (c, Left(a)) => f(c, a)
209 | case (c, Right(b)) => g(c, b)
210 | }
211 | def bifoldRight[A, B, C](fab: Selection[F,A,B],c: Eval[C])(f: (A, Eval[C]) => Eval[C],g: (B, Eval[C]) => Eval[C]): Eval[C] =
212 | fab.unwrap.foldRight(c){
213 | case (Left(a), ec) => f(a, ec)
214 | case (Right(b), ec) => g(b, ec)
215 | }
216 |
217 | def bitraverse[G[_]: Applicative, A, B, C, D](fab: Selection[F,A,B])(f: A => G[C], g: B => G[D]): G[Selection[F,C,D]] =
218 | fab.unwrap.traverse{
219 | case Left(a) => f(a).map(Either.left[C, D])
220 | case Right(b) => g(b).map(Either.right[C, D])
221 | }.map(Selection(_))
222 | }
223 |
224 | implicit def monadSelection[F[_]: Monad, B]: Monad[Selection[F, B, ?]] =
225 | new Monad[Selection[F, B, ?]]{
226 | def tailRecM[A, C](a: A)(f: A => Selection[F,B,Either[A,C]]): Selection[F,B,C] = Selection(
227 | Monad[F].tailRecM(a)(f(_).unwrap.map{
228 | case Left(l) => Right(Left(l))
229 | case Right(Left(a1)) => Left(a1)
230 | case Right(Right(b)) => Right(Right(b))
231 | })
232 | )
233 | def pure[A](x: A): Selection[F,B,A] = Selection(x.pure[F].map(Either.right))
234 | def flatMap[A, C](fa: Selection[F,B,A])(f: A => Selection[F,B,C]):Selection[F,B,C] =
235 | Selection(fa.unwrap.flatMap(_.fold(Either.left[B, C](_).pure[F], f(_).unwrap)))
236 | }
237 | }
238 |
239 | abstract private[selection] class SelectionInstances2 {
240 | implicit def foldableBiFoldableSelection[F[_]: Foldable]: Bifoldable[Selection[F, ?, ?]] =
241 | new Bifoldable[Selection[F, ?, ?]]{
242 | def bifoldLeft[A, B, C](fab: Selection[F,A,B],c: C)(f: (C, A) => C,g: (C, B) => C): C =
243 | fab.unwrap.foldLeft(c){
244 | case (c, Left(a)) => f(c, a)
245 | case (c, Right(b)) => g(c, b)
246 | }
247 | def bifoldRight[A, B, C](fab: Selection[F,A,B],c: Eval[C])(f: (A, Eval[C]) => Eval[C],g: (B, Eval[C]) => Eval[C]): Eval[C] =
248 | fab.unwrap.foldRight(c){
249 | case (Left(a), ec) => f(a, ec)
250 | case (Right(b), ec) => g(b, ec)
251 | }
252 | }
253 |
254 | implicit def functorSelection[F[_]: Functor, B]: Functor[Selection[F,B, ?]] =
255 | new Functor[Selection[F, B, ?]]{
256 | def map[A, C](fa: Selection[F, B,A])(f: A => C): Selection[F, B, C] =
257 | Selection(fa.unwrap.map(_.map(f)))
258 | }
259 |
260 | }
--------------------------------------------------------------------------------