Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cats: implement cats contrib package #5

Merged
merged 1 commit into from
Aug 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ lazy val tristate = (
settings(
packagedArtifacts := Map.empty // don't publish the default aggregate root project
)
aggregate(core, play, scalaz)
dependsOn(core, play, scalaz)
aggregate(core, play, cats, scalaz)
dependsOn(core, play, cats, scalaz)
)

lazy val core = (
Expand All @@ -31,6 +31,19 @@ lazy val play = (
dependsOn(core % "compile->compile;test->test")
)

lazy val cats = (
TristateProject("tristate-cats")
settings(
name := "tristate-cats",
libraryDependencies ++= Libs.at(scalaVersion.value)(
Libs.cats,
Libs.catsLaws,
Libs.scalaTest
)
)
dependsOn(core % "compile->compile;test->test")
)

lazy val scalaz = (
TristateProject("tristate-scalaz")
settings(
Expand Down
6 changes: 5 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ object Dependencies {
"2.12" -> "2.6.3")(key(sv))

val scalaCheck = (sv: String) => "1.12.6"
val scalaTest = (sv: String) => "3.0.3"
val specs2 = (sv: String) => "3.9.4"

def key(sv: String) =
Expand All @@ -29,10 +30,13 @@ object Dependencies {
val scalaz = (sv: String) => "org.scalaz" %% "scalaz-core" % V.scalaz(sv)
val scalazScalaCheck = (sv: String) => "org.scalaz" %% "scalaz-scalacheck-binding" % V.scalaz(sv) % "test"

val cats = (sv: String) => "org.typelevel" %% "cats" % V.cats(sv)
val cats = (sv: String) => "org.typelevel" %% "cats-core" % V.cats(sv)
val catsLaws = (sv: String) => "org.typelevel" %% "cats-laws" % V.cats(sv) % "test"

val playJson = (sv: String) => "com.typesafe.play" %% "play-json" % V.play(sv)

val scalaCheck = (sv: String) => "org.scalacheck" %% "scalacheck" % V.scalaCheck(sv) % "test"
val scalaTest = (sv: String) => "org.scalatest" %% "scalatest" % V.scalaTest(sv) % "test"
val specs2 = (sv: String) => "org.specs2" %% "specs2-core" % V.specs2(sv) % "test"

def at(sv: String)(libs: String => ModuleID*) =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
* Copyright 2016 David R. Bild
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.davidbild.tristate.contrib.cats

import org.davidbild.tristate.Tristate
import org.davidbild.tristate.Tristate._

import cats._
import cats.data.{Ior, Validated, ValidatedNel}

import scala.annotation.tailrec

trait TristateInstances extends TristateInstances2 {

implicit val tristateCatsInstancesForTristate: Traverse[Tristate] with MonadError[Tristate, Unit] with Monad[Tristate] with CoflatMap[Tristate] with MonoidK[Tristate] =
new Traverse[Tristate] with MonadError[Tristate, Unit] with Monad[Tristate] with CoflatMap[Tristate] with MonoidK[Tristate] {

def empty[A]: Tristate[A] = Absent

def combineK[A](x: Tristate[A], y: Tristate[A]): Tristate[A] =
x.cata(Present(_), y, y orElse Unspecified)

def pure[A](x: A): Tristate[A] = Present(x)

override def map[A, B](fa: Tristate[A])(f: A => B): Tristate[B] =
fa.map(f)

def flatMap[A, B](fa: Tristate[A])(f: A => Tristate[B]): Tristate[B] =
fa.flatMap(f)

@tailrec
def tailRecM[A, B](x: A)(f: A => Tristate[Either[A, B]]): Tristate[B] =
f(x) match {
case Unspecified => Unspecified
case Absent => Absent
case Present(Left(a)) => tailRecM(a)(f)
case Present(Right(b)) => Present(b)
}

override def map2[A, B, Z](fa: Tristate[A], fb: Tristate[B])(f: (A, B) => Z): Tristate[Z] =
fa.flatMap(a => fb.map(b => f(a, b)))

override def map2Eval[A, B, Z](fa: Tristate[A], fb: Eval[Tristate[B]])(f: (A, B) => Z): Eval[Tristate[Z]] =
fa match {
case Unspecified => Now(Unspecified)
case Absent => Now(Absent)
case Present(a) => fb.map(_.map(f(a, _)))
}

def coflatMap[A, B](fa: Tristate[A])(f: Tristate[A] => B): Tristate[B] =
fa.cobind(f)

def foldLeft[A, B](fa: Tristate[A], b: B)(f: (B, A) => B): B =
fa.cata(f(b, _), b, b)

def foldRight[A, B](fa: Tristate[A],lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa.cata(f(_, lb), lb, lb)

def raiseError[A](e: Unit): Tristate[A] = Absent

def handleErrorWith[A](fa: Tristate[A])(f: (Unit) => Tristate[A]): Tristate[A] = fa orElse f(())

def traverse[G[_]: Applicative, A, B](fa: Tristate[A])(f: A => G[B]): G[Tristate[B]] = {
val G = Applicative[G]
fa.cata(a => G.map(f(a))(Present(_)), G.pure(Absent), G.pure(Unspecified))
}

override def reduceLeftToOption[A, B](fa: Tristate[A])(f: A => B)(g: (B, A) => B): Option[B] =
fa.map(f).toOption

override def reduceRightToOption[A, B](fa: Tristate[A])(f: A => B)(g: (A, Eval[B]) => Eval[B]): Eval[Option[B]] =
Now(fa.map(f).toOption)

override def reduceLeftOption[A](fa: Tristate[A])(f: (A, A) => A): Option[A] = fa.toOption

override def reduceRightOption[A](fa: Tristate[A])(f: (A, Eval[A]) => Eval[A]): Eval[Option[A]] =
Now(fa.toOption)

override def minimumOption[A](fa: Tristate[A])(implicit A: Order[A]): Option[A] = fa.toOption

override def maximumOption[A](fa: Tristate[A])(implicit A: Order[A]): Option[A] = fa.toOption

override def get[A](fa: Tristate[A])(idx: Long): Option[A] =
if (idx == 0L) fa.toOption else None

override def size[A](fa: Tristate[A]): Long = fa.cata(_ => 1L, 0L, 0L)

override def foldMap[A, B](fa: Tristate[A])(f: A => B)(implicit B: Monoid[B]): B =
fa.cata(f, B.empty, B.empty)

override def find[A](fa: Tristate[A])(f: A => Boolean): Option[A] =
fa.filter(f).toOption

override def exists[A](fa: Tristate[A])(p: A => Boolean): Boolean =
fa.exists(p)

override def forall[A](fa: Tristate[A])(p: A => Boolean): Boolean =
fa.forall(p)

override def toList[A](fa: Tristate[A]): List[A] =
fa.toList

override def filter_[A](fa: Tristate[A])(p: A => Boolean): List[A] =
fa.filter(p).toList

override def takeWhile_[A](fa: Tristate[A])(p: A => Boolean): List[A] =
fa.filter(p).toList

override def dropWhile_[A](fa: Tristate[A])(p: A => Boolean): List[A] =
fa.filterNot(p).toList

override def isEmpty[A](fa: Tristate[A]): Boolean =
!fa.isPresent
}

implicit def tristateCatsShowForTristate[A](implicit A: Show[A]): Show[Tristate[A]] =
new TristateShow[A]
}

trait TristateInstances2 extends TristateInstances1 {
implicit def tristateCatsOrderForTristate[A: Order]: Order[Tristate[A]] =
new TristateOrder[A]
implicit def tristateCatsMonoidForTristate[A: Semigroup]: Monoid[Tristate[A]] =
new TristateMonoid[A]
}

trait TristateInstances1 extends TristateInstances0 {
implicit def tristateCatsPartialOrderForTristate[A: PartialOrder]: PartialOrder[Tristate[A]] =
new TristatePartialOrder[A]
}

trait TristateInstances0 {
implicit def tristateCatsEqForTristate[A: Eq]: Eq[Tristate[A]] =
new TristateEq[A]
}

class TristateShow[A](implicit A: Show[A]) extends Show[Tristate[A]] {
def show(fa: Tristate[A]): String =
fa.cata(a => s"Present(${A.show(a)})", "Absent", "Unspecified")
}

class TristateOrder[A](implicit A: Order[A]) extends Order[Tristate[A]] {
def compare(x: Tristate[A], y: Tristate[A]): Int =
(x, y) match {
case (Present(a) , Present(b)) => A.compare(a, b)
case (_ , Present(_)) => -1
case (Present(_) , _) => 1
case (Absent , Absent) => 0
case (Absent , Unspecified) => 1
case (Unspecified, Absent) => -1
case (Unspecified, Unspecified) => 0
}
}

class TristatePartialOrder[A](implicit A: PartialOrder[A]) extends PartialOrder[Tristate[A]] {
def partialCompare(x: Tristate[A], y: Tristate[A]): Double =
(x, y) match {
case (Present(a) , Present(b)) => A.partialCompare(a, b)
case (_ , Present(_)) => -1.0
case (Present(_) , _) => 1.0
case (Absent , Absent) => 0.0
case (Absent , Unspecified) => 1.0
case (Unspecified, Absent) => -1.0
case (Unspecified, Unspecified) => 0.0
}
}

class TristateEq[A](implicit A: Eq[A]) extends Eq[Tristate[A]] {
def eqv(x: Tristate[A], y: Tristate[A]): Boolean =
(x, y) match {
case (Unspecified, Unspecified) => true
case (Absent , Absent) => true
case (Present(a) , Present(b)) => A.eqv(a, b)
case (_ , _) => false
}
}

class TristateMonoid[A](implicit A: Semigroup[A]) extends Monoid[Tristate[A]] {
def empty: Tristate[A] = Unspecified
def combine(x: Tristate[A], y: Tristate[A]): Tristate[A] =
(x, y) match {
case (Unspecified, _) => y
case (_ , Unspecified) => x
case (Absent , _) => y
case (_ , Absent) => x
case (Present(a) , Present(b)) => Present(A.combine(a, b))
}
}

trait TristateSyntax {
implicit final def tristateCatsSyntaxTristate[A](fa: Tristate[A]): TristateOps[A] = new TristateOps(fa)
}

final class TristateOps[A](val fa: Tristate[A]) extends AnyVal {
/**
* If the `Tristate` is `Present`, return its value in a
* [[cats.data.Validated.Invalid]].
* If the `Tristate` is `Absent` or `Unspecified`, return the
* provided `B` value in a [[cats.data.Validated.Valid]].
*/
def toInvalid[B](b: => B): Validated[A, B] =
fa.cata(Validated.Invalid(_), Validated.Valid(b), Validated.Valid(b))

/**
* If the `Tristate` is `Present`, wrap its value in a
* [[cats.data.NonEmptyList]] and return it in a
* [[cats.data.Validated.Invalid]].
* If the `Tristate` is `Absent` or `Unspecified`, return
* the provided `B` value in a [[cats.data.Validated.Valid]].
*/
def toInvalidNel[B](b: => B): ValidatedNel[A, B] =
fa.cata(Validated.invalidNel(_), Validated.Valid(b), Validated.Valid(b))

/**
* If the `Tristate` is `Present`, return its value in a
* [[cats.data.Validated.Valid]].
* If the `Tristate` is `Absent` or `Unspecified`, return
* the provided `B` value in a [[cats.data.Validated.Invalid]].
*/
def toValid[B](b: => B): Validated[B, A] =
fa.cata(Validated.Valid(_), Validated.Invalid(b), Validated.Invalid(b))

/**
* If the `Tristate` is `Present`, return its value in a
* [[cats.data.Validated.Valid]].
* If the `Tristate` is `Absent` or `Unspecified`, wrap
* the provided `B` value in a [[cats.data.NonEmptyList]]
* and return the result in a [[cats.data.Validated.Invalid]].
*/
def toValidNel[B](b: => B): ValidatedNel[B, A] =
fa.cata(Validated.Valid(_), Validated.invalidNel(b), Validated.invalidNel(b))

/**
* If the `Tristate` is `Present`, return its value in a
* [[cats.data.Ior.Right]].
* If the `Tristate` is `Absent` or `Unspecified`, wrap
* the provided `B` value in a [[cats.data.Ior.Left]].
*/
def toRightIor[B](b: => B): Ior[B, A] =
fa.cata(Ior.Right(_), Ior.Left(b), Ior.Left(b))

/**
* If the `Tristate` is `Present`, return its value in a
* [[cats.data.Ior.Left]].
* If the `Tristate` is `Absent` or `Unspecified`, wrap
* the provided `B` value in a [[cats.data.Ior.Right]].
*/
def toLeftIor[B](b: => B): Ior[A, B] =
fa.cata(Ior.Left(_), Ior.Right(b), Ior.Right(b))

/**
* If the `Tristate` is `Present`, return its value. If
* the `Tristate` is `Absent` or `Unspecified, return the
* `empty` value for `Monoid[A]`.
*/
def orEmpty(implicit A: Monoid[A]): A =
fa.getOrElse(A.empty)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.davidbild.tristate.contrib

package object cats extends TristateInstances with TristateSyntax
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.davidbild.tristate.contrib.cats

import cats.implicits._
import cats.laws.discipline._
import cats.kernel.laws.OrderLaws

import org.scalacheck.{Arbitrary, Cogen, Gen}
import org.scalatest.{FunSuite, Matchers}
import org.scalatest.prop._
import org.typelevel.discipline.scalatest.Discipline

import org.scalacheck.rng.Seed

import org.davidbild.tristate._
import org.davidbild.tristate.Tristate._

class TristateTests extends FunSuite with Matchers with PropertyChecks with Discipline {

checkAll("Tristate[Int]", CartesianTests[Tristate].cartesian[Int, Int, Int])
checkAll("Tristate[Int] with Option", TraverseTests[Tristate].traverse[Int, Int, Int, Int, Tristate, Option])
checkAll("Tristate with Unit", MonadErrorTests[Tristate, Unit].monadError[Int, Int, Int])
checkAll("Tristate[Int]", ApplicativeTests[Tristate].applicative[Int, Int, Int])
checkAll("Tristate[Int]", MonadTests[Tristate].monad[Int, Int, Int])
checkAll("Tristate[Int]", MonoidKTests[Tristate].monoidK[Int])
checkAll("Tristate[Int]", CoflatMapTests[Tristate].coflatMap[Int, Int, Int])

val orderLaws = OrderLaws[Tristate[Int]]
checkAll("Tristate[Int]", orderLaws.partialOrder)
checkAll("Tristate[Int]", orderLaws.order)

test("show") {
absent[String].show should === ("Absent")
unspecified[String].show should === ("Unspecified")

forAll { fs: Tristate[String] =>
fs.show should === (fs.toString)
}
}

private implicit def cogenTristate[A: Cogen]: Cogen[Tristate[A]] = {
val A = implicitly[Cogen[A]]
Cogen((seed: Seed, t: Tristate[A]) => t.cata(a => A.perturb(seed.next, a), seed, seed))
}

private implicit def arbTristate[A: Arbitrary]: Arbitrary[Tristate[A]] = {
val A = implicitly[Arbitrary[A]]
Arbitrary(Gen.sized(n =>
Gen.frequency(
(1, A.arbitrary.map(Present(_))),
(1, Gen.const(Absent)),
(1, Gen.const(Unspecified))
)
))
}

}