Skip to content

Commit dfdd376

Browse files
Merge pull request #19 from morgen-peschke/add-quality-of-life-syntax-for-cats
Add quality of life syntax for cats
2 parents a162774 + f5a458f commit dfdd376

File tree

3 files changed

+411
-38
lines changed

3 files changed

+411
-38
lines changed

core/src/peschke/cats/syntax.scala

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package peschke.cats
2+
3+
import cats.Applicative
4+
import cats.FlatMap
5+
import cats.Functor
6+
import cats.Order
7+
import cats.data.Validated
8+
import cats.kernel.Eq
9+
import cats.syntax.all._
10+
import cats.~>
11+
12+
object syntax {
13+
implicit final class ScalaCommonsCatsNestedFunctorOps[F[_], G[_], A]
14+
(private val fga: F[G[A]])
15+
extends AnyVal {
16+
17+
/** Equivalent to `.map(_.map(_))`
18+
*
19+
* Handy for when you just need to do this once or twice, and a Transformer
20+
* or [[cats.data.Nested]] would be overkill.
21+
*/
22+
def innerMap[B](ab: A => B)(implicit F: Functor[F], G: Functor[G])
23+
: F[G[B]] = fga.map(_.map(ab))
24+
25+
/** Equivalent to `.map(_.flatMap(_))`
26+
*
27+
* Handy for when you just need to do this once or twice, and a Transformer
28+
* or [[cats.data.Nested]] would be overkill.
29+
*/
30+
def innerFlatMap[B](aGb: A => G[B])(implicit F: Functor[F], G: FlatMap[G])
31+
: F[G[B]] = fga.map(_.flatMap(aGb))
32+
33+
/** Basically [[cats.data.Nested.mapK]], but without the wrapper
34+
*/
35+
def mapK[H[_]](fh: F ~> H): H[G[A]] = fh(fga)
36+
37+
/** Basically [[mapK]], but operating on the inner `G` instead of the outer
38+
* `F`
39+
*/
40+
def innerMapK[H[_]](gh: G ~> H)(implicit F: Functor[F]): F[H[A]] =
41+
fga.map(ga => gh(ga))
42+
}
43+
44+
implicit final class ScalaCommonsCatsValidatedOps[E, A]
45+
(private val va: Validated[E, A])
46+
extends AnyVal {
47+
48+
/** [[cats.Traverse.flatTraverse]], but for [[Validated]]
49+
*
50+
* [[Validated]] doesn't have a [[FlatMap]] instance, so we can't use
51+
* [[cats.Traverse.flatTraverse]] directly, but sometimes that is something
52+
* really, really handy.
53+
*/
54+
def andThenF[F[_]: Applicative, B](f: A => F[Validated[E, B]])
55+
: F[Validated[E, B]] =
56+
va match {
57+
case Validated.Invalid(e) => e.invalid[B].pure[F]
58+
case Validated.Valid(a) => f(a)
59+
}
60+
61+
/** Exactly equivalent to [[Validated.traverse]]
62+
*
63+
* When paired with [[andThenF]], it can read better to use [[mapF]].
64+
*/
65+
def mapF[F[_]: Applicative, B](f: A => F[B]): F[Validated[E, B]] =
66+
va.traverse(f)
67+
}
68+
69+
implicit final class ScalaCommonsCatsOrderOps[A](private val order: Order[A])
70+
extends AnyVal {
71+
72+
/** Alias of [[cats.Order.reverse]], which can be a bit easier to chain.
73+
*/
74+
def reversed: Order[A] = Order.reverse(order)
75+
}
76+
77+
implicit final class ScalaCommonsCatsOrderObjOps(private val O: Order.type)
78+
extends AnyVal {
79+
80+
/** Build an [[Order]] from a bunch of existing [[Order]] instances.
81+
*
82+
* Equivalent to using [[cats.Order.whenEqual]] to combine them all, but
83+
* can be a bit easier to read and write because of it's flat nature.
84+
*
85+
* {{{
86+
* final case class Foo(i: Int, s: String, f: Float)
87+
* implicit val order: Order[Foo] =
88+
* Order
89+
* .builder[Foo]
90+
* .by(_.i)
91+
* .andThen(Order.by(_.s).reversed)
92+
* .by(_.f)
93+
* .build
94+
* }}}
95+
*/
96+
def builder[A]: OrderUsingEmptyBuilder[A] = new OrderUsingEmptyBuilder[A](O)
97+
}
98+
99+
implicit final class ScalaCommonsCatsEqObjOps(private val E: Eq.type)
100+
extends AnyVal {
101+
102+
/** Build an [[Eq]] from a bunch of existing [[Eq]] instances.
103+
*
104+
* Equivalent to using [[Eq.and]] to combine them all, but can be a bit
105+
* easier to read and write because of it's flat nature.
106+
*
107+
* {{{
108+
* final case class Foo(i: Int, s: String, f: Float)
109+
* implicit val eq: Eq[Foo] =
110+
* Eq
111+
* .builder[Foo]
112+
* .by(_.i)
113+
* .and(Eq.instance(_.s equalsIgnoreCase _.s))
114+
* .by(_.f)
115+
* .build
116+
* }}}
117+
*/
118+
def builder[A]: EqUsingEmptyBuilder[A] = new EqUsingEmptyBuilder[A](E)
119+
}
120+
}
121+
122+
final class OrderUsingEmptyBuilder[A](private val O: Order.type)
123+
extends AnyVal {
124+
125+
/** Set the primary [[Order]] instance
126+
*/
127+
def on(oa: Order[A]): OrderUsingBuilder[A] =
128+
new OrderUsingBuilder[A](oa)
129+
130+
/** Sugar for `on(Order.by(fab))`
131+
*/
132+
def by[B: Order](fab: A => B): OrderUsingBuilder[A] =
133+
on(O.by(fab))
134+
}
135+
final class OrderUsingBuilder[A](private val accum: Order[A]) extends AnyVal {
136+
137+
/** Add an additional [[Order]] tie-breaker
138+
*/
139+
def andThen(oa: Order[A]): OrderUsingBuilder[A] =
140+
new OrderUsingBuilder[A](Order.whenEqual(accum, oa))
141+
142+
/** Sugar for `andThen(Order.by(fab))`
143+
*/
144+
def by[B: Order](fab: A => B): OrderUsingBuilder[A] =
145+
andThen(Order.whenEqual(accum, Order.by(fab)))
146+
147+
/** Unwrap the resulting [[Order]]
148+
*/
149+
def build: Order[A] = accum
150+
}
151+
152+
final class EqUsingEmptyBuilder[A](private val E: Eq.type) extends AnyVal {
153+
154+
/** Set the primary [[Eq]] instance
155+
*/
156+
def on(oa: Eq[A]): EqUsingBuilder[A] = new EqUsingBuilder[A](oa)
157+
158+
/** Sugar for `on(Eq.by(fab))`
159+
*/
160+
def by[B: Eq](fab: A => B): EqUsingBuilder[A] =
161+
new EqUsingBuilder[A](E.by(fab))
162+
}
163+
final class EqUsingBuilder[A](private val accum: Eq[A]) extends AnyVal {
164+
165+
/** Add an additional [[Eq]] instance that must be satisfied
166+
*/
167+
def and(oa: Eq[A]): EqUsingBuilder[A] =
168+
new EqUsingBuilder[A](Eq.and(accum, oa))
169+
170+
/** Sugar for `and(Eq.by(fab))`
171+
*/
172+
def by[B: Eq](fab: A => B): EqUsingBuilder[A] =
173+
new EqUsingBuilder[A](Eq.and(accum, Eq.by(fab)))
174+
175+
/** Unwrap the resulting [[Eq]]
176+
*/
177+
def build: Eq[A] = accum
178+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package peschke.cats
2+
3+
import cats.Comparison
4+
import cats.Eq
5+
import cats.Order
6+
import cats.data.Validated
7+
import cats.syntax.all._
8+
import cats.~>
9+
import org.scalatest.prop.TableFor2
10+
import peschke.TableSpec
11+
import peschke.cats.syntax._
12+
import peschke.cats.syntaxTest.Foo
13+
14+
class syntaxTest extends TableSpec {
15+
"F[G[A]].innerMap" should {
16+
"enable modifying the contained value" in {
17+
List(
18+
5.asRight,
19+
"ignored".asLeft,
20+
0.asRight
21+
).innerMap(_ + 5) mustBe List(10.asRight, "ignored".asLeft, 5.asRight)
22+
}
23+
}
24+
25+
"F[G[A]].innerFlatMap" should {
26+
"enable modifying the inner effect" in {
27+
List(
28+
5.asRight,
29+
"ignored".asLeft,
30+
0.asRight
31+
).innerFlatMap(v => if (v eqv 0) "<0>".asLeft else v.asRight) mustBe List(
32+
5.asRight,
33+
"ignored".asLeft,
34+
"<0>".asLeft
35+
)
36+
}
37+
}
38+
39+
"F[G[A]].mapK" should {
40+
"allow changing the outer effect, while retaining the inner effect" in {
41+
5.asRight[String].some.mapK(Lambda[Option ~> List](_.toList)) mustBe List(
42+
5.asRight[String]
43+
)
44+
45+
List(5.asRight[String], "discarded".asLeft[Int])
46+
.mapK(Lambda[List ~> Option](_.headOption)) mustBe 5
47+
.asRight[String].some
48+
}
49+
}
50+
51+
"F[G[A]].innerMapK" should {
52+
"allow changing the inner effect, while retaining the outer effect" in {
53+
List(
54+
5.asRight,
55+
"discarded".asLeft,
56+
0.asRight
57+
).innerMapK(
58+
Lambda[Lambda[r => Either[String, r]] ~> Option](_.toOption)
59+
) mustBe List(
60+
5.some,
61+
none,
62+
0.some
63+
)
64+
}
65+
}
66+
67+
"Validated.andThenF" should {
68+
val t
69+
: TableFor2[Validated[String, Int], Int => Option[Validated[String, Int]]] =
70+
Table[Validated[String, Int], Int => Option[Validated[String, Int]]](
71+
("input", "function"),
72+
(5.valid, _.valid.some),
73+
("hi".invalid, _.valid.some),
74+
(5.valid, i => s"$i".invalid.some),
75+
("hi".invalid, i => s"$i".invalid.some),
76+
(5.valid, _ => none),
77+
("hi".invalid, _ => none)
78+
)
79+
80+
"behave the same way as flatTraverse does for Either" in forAll(t) {
81+
(validatedInput, validatedFunction) =>
82+
val eitherInput: Either[String, Int] = validatedInput.toEither
83+
val eitherFunction: Int => Option[Either[String, Int]] =
84+
validatedFunction(_).map(_.toEither)
85+
86+
val validatedOutput = validatedInput.andThenF(validatedFunction)
87+
val eitherOutput = eitherInput.flatTraverse(eitherFunction)
88+
89+
validatedOutput.map(_.toEither) mustBe eitherOutput
90+
eitherOutput.map(_.toValidated) mustBe validatedOutput
91+
}
92+
}
93+
94+
"Order.builder" should {
95+
"add subsequent instances as tiebreakers, in the correct order" in {
96+
val onlyS = Order.by[Foo, String](_.s)
97+
val onlyI = Order.builder[Foo].by(_.i).build
98+
val onlyIS = Order.builder[Foo].by(_.i).andThen(onlyS).build
99+
val all = Order.builder[Foo].by(_.i).by(_.s).by(_.c).build
100+
101+
onlyI.comparison(
102+
Foo(4, "a", 'b'),
103+
Foo(4, "z", 'z')
104+
) mustBe Comparison.EqualTo
105+
onlyI.comparison(
106+
Foo(4, "z", 'z'),
107+
Foo(5, "a", 'a')
108+
) mustBe Comparison.LessThan
109+
onlyI.comparison(
110+
Foo(4, "a", 'a'),
111+
Foo(3, "z", 'z')
112+
) mustBe Comparison.GreaterThan
113+
114+
withClue("[Should defer to i]") {
115+
onlyIS.comparison(
116+
Foo(4, "z", 'z'),
117+
Foo(5, "a", 'a')
118+
) mustBe Comparison.LessThan
119+
onlyIS.comparison(
120+
Foo(4, "a", 'a'),
121+
Foo(3, "z", 'z')
122+
) mustBe Comparison.GreaterThan
123+
}
124+
onlyIS.comparison(
125+
Foo(4, "a", 'z'),
126+
Foo(4, "z", 'a')
127+
) mustBe Comparison.LessThan
128+
onlyIS.comparison(
129+
Foo(4, "z", 'a'),
130+
Foo(4, "a", 'z')
131+
) mustBe Comparison.GreaterThan
132+
onlyIS.comparison(
133+
Foo(4, "a", 'z'),
134+
Foo(4, "a", 'z')
135+
) mustBe Comparison.EqualTo
136+
137+
withClue("[Should defer to i]") {
138+
all.comparison(
139+
Foo(4, "z", 'z'),
140+
Foo(5, "a", 'a')
141+
) mustBe Comparison.LessThan
142+
all.comparison(
143+
Foo(4, "a", 'a'),
144+
Foo(3, "z", 'z')
145+
) mustBe Comparison.GreaterThan
146+
}
147+
withClue("[Should defer to s]") {
148+
all.comparison(
149+
Foo(4, "a", 'z'),
150+
Foo(4, "z", 'a')
151+
) mustBe Comparison.LessThan
152+
all.comparison(
153+
Foo(4, "z", 'a'),
154+
Foo(4, "a", 'z')
155+
) mustBe Comparison.GreaterThan
156+
}
157+
all.comparison(
158+
Foo(4, "a", 'a'),
159+
Foo(4, "a", 'z')
160+
) mustBe Comparison.LessThan
161+
all.comparison(
162+
Foo(4, "a", 'z'),
163+
Foo(4, "a", 'a')
164+
) mustBe Comparison.GreaterThan
165+
all.comparison(
166+
Foo(4, "a", 'a'),
167+
Foo(4, "a", 'a')
168+
) mustBe Comparison.EqualTo
169+
}
170+
}
171+
172+
"Eq.builder" should {
173+
"require all instances to agree something is equal to return true" in {
174+
val onlyS = Eq.by[Foo, String](_.s)
175+
val onlyI = Eq.builder[Foo].by(_.i).build
176+
val onlyIS = Eq.builder[Foo].by(_.i).and(onlyS).build
177+
val all = Eq.builder[Foo].by(_.i).by(_.s).by(_.c).build
178+
179+
onlyI.eqv(Foo(4, "a", 'a'), Foo(3, "a", 'a')) mustBe false
180+
onlyI.eqv(Foo(4, "a", 'a'), Foo(4, "z", 'z')) mustBe true
181+
182+
onlyIS.eqv(Foo(4, "a", 'a'), Foo(3, "a", 'a')) mustBe false
183+
onlyIS.eqv(Foo(4, "a", 'a'), Foo(4, "z", 'a')) mustBe false
184+
onlyIS.eqv(Foo(4, "a", 'z'), Foo(4, "a", 'a')) mustBe true
185+
186+
all.eqv(Foo(4, "a", 'a'), Foo(3, "a", 'a')) mustBe false
187+
all.eqv(Foo(4, "a", 'a'), Foo(4, "z", 'a')) mustBe false
188+
all.eqv(Foo(4, "a", 'z'), Foo(4, "a", 'a')) mustBe false
189+
all.eqv(Foo(4, "a", 'a'), Foo(4, "a", 'a')) mustBe true
190+
}
191+
}
192+
}
193+
object syntaxTest {
194+
final case class Foo(i: Int, s: String, c: Char)
195+
}

0 commit comments

Comments
 (0)