diff --git a/build.sbt b/build.sbt index 82502dbd7..93bf2fda7 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ val pureconfigVersion = "0.9.0" val shapelessVersion = "2.3.3" val scalaCheckVersion = "1.13.5" val scalaXmlVersion = "1.0.6" -val scalazVersion = "7.2.19" +val scalazVersion = "7.2.20" val scodecVersion = "1.10.3" val macroParadise = diff --git a/modules/scalaz/shared/src/main/scala/eu/timepit/refined/scalaz/package.scala b/modules/scalaz/shared/src/main/scala/eu/timepit/refined/scalaz/package.scala index 49d5915f3..74ed817a8 100644 --- a/modules/scalaz/shared/src/main/scala/eu/timepit/refined/scalaz/package.scala +++ b/modules/scalaz/shared/src/main/scala/eu/timepit/refined/scalaz/package.scala @@ -1,10 +1,11 @@ +// Copyright: 2015 - 2018 Frank S. Thomas and Sam Halliday +// License: https://opensource.org/licenses/MIT + package eu.timepit.refined -import _root_.scalaz.Equal -import _root_.scalaz.Show -import _root_.scalaz.@@ +import _root_.scalaz.{@@, Contravariant, Equal, MonadError, Show} import _root_.scalaz.syntax.contravariant._ -import eu.timepit.refined.api.RefType +import eu.timepit.refined.api.{RefType, Validate} import scala.reflect.macros.blackbox package object scalaz { @@ -42,4 +43,33 @@ package object scalaz { */ implicit def refTypeShow[F[_, _], T: Show, P](implicit rt: RefType[F]): Show[F[T, P]] = Show[T].contramap(rt.unwrap) + + /** + * Instances for typeclasses with a `Contravariant`, e.g. encoders. + */ + implicit def refTypeContravariant[R[_, _], F[_], A, B]( + implicit + C: Contravariant[F], + R: RefType[R], + F: F[A] + ): F[R[A, B]] = C.contramap(F)(R.unwrap) + + /** + * Instances for typeclasses with a `MonadError[?, String]`, i.e. a + * disjunction kleisli arrow applied to the typeclass. e.g. decoders. + */ + implicit def refTypeMonadError[R[_, _], F[_], A, B]( + implicit + M: MonadError[F, String], + R: RefType[R], + V: Validate[A, B], + F: F[A] + ): F[R[A, B]] = + M.bind(F) { f => + R.refine(f) match { + case Left(s) => M.raiseError(s) + case Right(v) => M.pure(v) + } + } + } diff --git a/modules/scalaz/shared/src/test/scala-2.12/eu/timepit/refined/scalaz/RefTypeSpecScalazMonadError.scala b/modules/scalaz/shared/src/test/scala-2.12/eu/timepit/refined/scalaz/RefTypeSpecScalazMonadError.scala new file mode 100644 index 000000000..dd82875b7 --- /dev/null +++ b/modules/scalaz/shared/src/test/scala-2.12/eu/timepit/refined/scalaz/RefTypeSpecScalazMonadError.scala @@ -0,0 +1,57 @@ +// Copyright: 2018 Sam Halliday +// License: https://opensource.org/licenses/MIT + +package eu.timepit.refined.scalaz + +import _root_.scalaz.{@@, \/, IsomorphismMonadError, MonadError, ReaderT} +import _root_.scalaz.Isomorphism.{<~>, IsoFunctorTemplate} +import _root_.scalaz.syntax.either._ +import eu.timepit.refined.api._ +import eu.timepit.refined.collection._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +trait Decoder[A] { + def decode(s: String): String \/ A +} +object Decoder { + @inline def apply[A](implicit A: Decoder[A]): Decoder[A] = A + @inline def instance[A](f: String => String \/ A): Decoder[A] = + new Decoder[A] { + override def decode(s: String): String \/ A = f(s) + } + + implicit val string: Decoder[String] = instance(_.right) + + type Out[a] = String \/ a + type MT[a] = ReaderT[Out, String, a] + implicit val isoReaderT: Decoder <~> MT = + new IsoFunctorTemplate[Decoder, MT] { + def from[A](fa: MT[A]) = instance(fa.run(_)) + def to[A](fa: Decoder[A]) = ReaderT[Out, String, A](fa.decode) + } + // will be cleaner after https://github.com/scalaz/scalaz/pull/1661 + private[this] val ME = MonadError[MT, String] + implicit def monad: MonadError[Decoder, String] = + new IsomorphismMonadError[Decoder, MT, String] { + override implicit val G: MonadError[MT, String] = ME + override val iso: Decoder <~> MT = isoReaderT + } +} + +class RefTypeSpecScalazMonadError extends Properties("scalaz.Contravariant") { + // annoying that this import is needed! + // https://github.com/scala/bug/issues/10753#issuecomment-369592913 + import Decoder.monad + + property("Refined via scalaz.MonadError[?, String]") = secure { + val decoder = Decoder[String Refined NonEmpty] + decoder.decode("").isLeft && decoder.decode("hello world").isRight + } + + property("@@ via scalaz.MonadError[?, String]") = secure { + val decoder = Decoder[String @@ NonEmpty] + decoder.decode("").isLeft && decoder.decode("hello world").isRight + } + +} diff --git a/modules/scalaz/shared/src/test/scala/eu/timepit/refined/scalaz/RefTypeSpecScalazTag.scala b/modules/scalaz/shared/src/test/scala-2.12/eu/timepit/refined/scalaz/RefTypeSpecScalazTag.scala similarity index 100% rename from modules/scalaz/shared/src/test/scala/eu/timepit/refined/scalaz/RefTypeSpecScalazTag.scala rename to modules/scalaz/shared/src/test/scala-2.12/eu/timepit/refined/scalaz/RefTypeSpecScalazTag.scala diff --git a/modules/scalaz/shared/src/test/scala/eu/timepit/refined/scalaz/RefTypeSpecScalazContravariant.scala b/modules/scalaz/shared/src/test/scala/eu/timepit/refined/scalaz/RefTypeSpecScalazContravariant.scala new file mode 100644 index 000000000..99f0a49ac --- /dev/null +++ b/modules/scalaz/shared/src/test/scala/eu/timepit/refined/scalaz/RefTypeSpecScalazContravariant.scala @@ -0,0 +1,51 @@ +// Copyright: 2018 Sam Halliday +// License: https://opensource.org/licenses/MIT + +package eu.timepit.refined.scalaz + +import _root_.scalaz.{@@, Contravariant} +import eu.timepit.refined.api._ +import eu.timepit.refined.collection._ +import org.scalacheck._ +import org.scalacheck.Prop._ + +trait Encoder[A] { + def encode(a: A): String +} +object Encoder { + @inline def apply[A](implicit A: Encoder[A]): Encoder[A] = A + @inline def instance[A](f: A => String): Encoder[A] = new Encoder[A] { + override def encode(a: A): String = f(a) + } + + implicit val string: Encoder[String] = instance(identity) + + implicit val contravariant: Contravariant[Encoder] = + new Contravariant[Encoder] { + override def contramap[A, B](fa: Encoder[A])(f: B => A): Encoder[B] = + instance(b => fa.encode(f(b))) + } +} + +class RefTypeSpecScalazContravariant extends Properties("scalaz.Contravariant") { + // annoying that this import is needed! + // https://github.com/scala/bug/issues/10753#issuecomment-369592913 + import Encoder._ + + property("Refined via scalaz.Contravariant") = secure { + import eu.timepit.refined.auto._ + + val x: String Refined NonEmpty = "hello world" + + Encoder[String Refined NonEmpty].encode(x) == "hello world" + } + + property("@@ via scalaz.Contravariant") = secure { + import eu.timepit.refined.scalaz.auto._ + + val x: String @@ NonEmpty = "hello world" + + Encoder[String @@ NonEmpty].encode(x) == "hello world" + } + +}