diff --git a/build.sbt b/build.sbt index 1447059..931c2e9 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,7 @@ val scalaV = scalaVersion := "2.12.5" val commons = Seq( scalaV, - version := "0.0.2", + version := "0.0.3", fork in Test := true, parallelExecution in Test := false, organization := "one.fluence", diff --git a/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala b/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala index 7002172..4c8cfc5 100644 --- a/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala +++ b/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala @@ -24,7 +24,7 @@ import cats.data.{EitherT, Kleisli} import cats.syntax.flatMap._ import cats.syntax.compose._ -import scala.language.higherKinds +import scala.language.{existentials, higherKinds} import scala.util.Try /** @@ -62,9 +62,10 @@ abstract class MonadicalEitherArrow[E <: Throwable] { * Run the func on input, lifting the error into MonadError effect. * * @param input Input - * @param F All internal maps and composes, as well as errors, are to be executed with this Monad + * @param F All internal maps and composes, as well as errors, are to be executed with this MonadError. + * Error type should be a supertype for this arrow's error E. */ - def runF[F[_]](input: A)(implicit F: MonadError[F, Throwable]): F[B] = + def runF[F[_]](input: A)(implicit F: MonadError[F, EE] forSome {type EE >: E}): F[B] = runEither(input).flatMap(F.fromEither) /** @@ -78,9 +79,10 @@ abstract class MonadicalEitherArrow[E <: Throwable] { /** * Converts this Func to Kleisli, using MonadError to execute upon and to lift errors into. * - * @param F All internal maps and composes, as well as errors, are to be executed with this Monad + * @param F All internal maps and composes, as well as errors, are to be executed with this MonadError. + * Error type should be a supertype for this arrow's error E. */ - def toKleisli[F[_]](implicit F: MonadError[F, Throwable]): Kleisli[F, A, B] = + def toKleisli[F[_]](implicit F: MonadError[F, EE] forSome {type EE >: E}): Kleisli[F, A, B] = Kleisli(input ⇒ runF[F](input)) /** @@ -93,6 +95,15 @@ abstract class MonadicalEitherArrow[E <: Throwable] { import cats.instances.try_._ runF[Try](input).get } + + /** + * Picks a point from the arrow, using the initial element (Unit) on the left. + * + * @param input Point to pick + * @return Picked point + */ + def pointAt(input: A): Point[B] = + catsMonadicalEitherArrowChoice.lmap(this)(_ ⇒ input) } /** @@ -182,6 +193,26 @@ abstract class MonadicalEitherArrow[E <: Throwable] { EitherT.rightT[F, E](input).subflatMap(f) } + /** + * Check a condition, lifted with a Func. + * + * @param error Error to produce when condition is not met + * @return Func that takes boolean, checks it, and returns Unit or fails with given error + */ + def cond(error: ⇒ E): Func[Boolean, Unit] = + liftFuncEither(Either.cond(_, (), error)) + + /** + * Lift a function which returns a Func arrow with Unit on the left side. + * + * @param f Function to lift + */ + def liftFuncPoint[A, B](f: A ⇒ Point[B]): Func[A,B] = + new Func[A,B]{ + override def apply[F[_] : Monad](input: A): EitherT[F, E, B] = + f(input).apply[F](()) + } + /** * Func that does nothing with input. */ @@ -215,6 +246,42 @@ abstract class MonadicalEitherArrow[E <: Throwable] { } } + /** + * Point type maps from Unit to a particular value of A, so it's just a lazy Func. + * + * @tparam A Output value type + */ + type Point[A] = Func[Unit, A] + + /** + * Point must obey MonadErrorLaws + */ + implicit object catsMonadicalEitherPointMonad extends MonadError[Point, E] { + override def flatMap[A, B](fa: Point[A])(f: A ⇒ Point[B]): Point[B] = + new Func[Unit, B]{ + override def apply[F[_] : Monad](input: Unit): EitherT[F, E, B] = + fa[F](()).flatMap(f(_).apply[F](())) + } + + override def tailRecM[A, B](a: A)(f: A ⇒ Point[Either[A, B]]): Point[B] = + new Func[Unit, B]{ + override def apply[F[_] : Monad](input: Unit): EitherT[F, E, B] = + Monad[EitherT[F, E, ?]].tailRecM(a)(f(_).apply[F](())) + } + + override def raiseError[A](e: E): Point[A] = + liftFuncEither(_ ⇒ Left(e)) + + override def handleErrorWith[A](fa: Point[A])(f: E ⇒ Point[A]): Point[A] = + new Func[Unit, A]{ + override def apply[F[_] : Monad](input: Unit): EitherT[F, E, A] = + fa[F](()).leftFlatMap(e ⇒ f(e).apply[F](())) + } + + override def pure[A](x: A): Point[A] = + liftFunc(_ ⇒ x) + } + /** * Lifts pure direct and inverse functions into Bijection. * @@ -237,6 +304,12 @@ abstract class MonadicalEitherArrow[E <: Throwable] { def liftEitherB[A, B](direct: A ⇒ Either[E, B], inverse: B ⇒ Either[E, A]): Bijection[A, B] = Bijection(liftFuncEither(direct), liftFuncEither(inverse)) + /** + * Lifts point functions into Bijection. + */ + def liftPointB[A,B](direct: A ⇒ Point[B], inverse: B ⇒ Point[A]): Bijection[A,B] = + Bijection(liftFuncPoint(direct), liftFuncPoint(inverse)) + /** * Bijection that does no transformation. */ diff --git a/core/src/test/scala/fluence/codec/PureCodecFuncTestInstances.scala b/core/src/test/scala/fluence/codec/PureCodecFuncTestInstances.scala index d8d2a33..3243463 100644 --- a/core/src/test/scala/fluence/codec/PureCodecFuncTestInstances.scala +++ b/core/src/test/scala/fluence/codec/PureCodecFuncTestInstances.scala @@ -23,6 +23,13 @@ import org.scalacheck.{Arbitrary, Cogen, Gen} import org.scalacheck.ScalacheckShapeless._ object PureCodecFuncTestInstances { + implicit def arbCodecError: Arbitrary[CodecError] = + Arbitrary(Gen.alphaLowerStr.map(CodecError(_))) + + implicit def eqCodecError: Eq[CodecError] = Eq.fromUniversalEquals + + implicit def cogenE: Cogen[CodecError] = Cogen.cogenString.contramap[CodecError](_.message) + implicit def arbFunc[A: Arbitrary: Cogen, B: Arbitrary]: Arbitrary[PureCodec.Func[A, B]] = Arbitrary( Gen diff --git a/core/src/test/scala/fluence/codec/PureCodecPointLawsSpec.scala b/core/src/test/scala/fluence/codec/PureCodecPointLawsSpec.scala new file mode 100644 index 0000000..e843dfc --- /dev/null +++ b/core/src/test/scala/fluence/codec/PureCodecPointLawsSpec.scala @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 Fluence Labs Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fluence.codec + +import cats.{Eq, Invariant} +import cats.data.EitherT +import cats.laws.discipline.{MonadErrorTests, SemigroupalTests} +import cats.tests.CatsSuite +import fluence.codec +import org.scalacheck.ScalacheckShapeless._ + +class PureCodecPointLawsSpec extends CatsSuite { + + import PureCodecFuncTestInstances._ + + implicit def eqEitherTFEA: Eq[EitherT[PureCodec.Point, CodecError, Int]] = + Eq.instance{ + case (aa,bb) ⇒ + aa.value.unsafe(()) == bb.value.unsafe(()) + } + + implicit val iso = SemigroupalTests.Isomorphisms.invariant[PureCodec.Point]( + new Invariant[PureCodec.Point]{ + override def imap[A, B](fa: codec.PureCodec.Point[A])(f: A ⇒ B)(g: B ⇒ A): codec.PureCodec.Point[B] = + fa.map(f) + } + ) + + checkAll( + "PureCodec.Point.MonadErrorLaws", + MonadErrorTests[PureCodec.Point, CodecError].monadError[Int, String, Double] + ) +}