├── 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 [![Build Status](https://travis-ci.com/ChristopherDavenport/selection.svg?branch=master)](https://travis-ci.com/ChristopherDavenport/selection) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/selection_2.12/badge.svg)](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 [![Build Status](https://travis-ci.com/ChristopherDavenport/selection.svg?branch=master)](https://travis-ci.com/ChristopherDavenport/selection) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.chrisdavenport/selection_2.12/badge.svg)](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 | } --------------------------------------------------------------------------------