7 Commits

Author SHA1 Message Date
8d905117c8 Kryo codecs example 2018-06-14 13:34:18 +03:00
60661d5555 Codec composition example 2018-06-13 16:45:31 +03:00
e4ab5f147f Few more examples 2018-06-13 16:21:12 +03:00
f23f9dcfab Added PureCodec examples 2018-06-13 13:56:09 +03:00
6efaac148b Merge pull request #3 from fluencelabs/point
Introducing Point monad
2018-05-21 13:54:20 +03:00
6378af3658 Docs for existential type bound for MonadError 2018-05-21 13:45:19 +03:00
e1b5bfb947 Introducing Point monad 2018-05-21 11:59:37 +03:00
8 changed files with 353 additions and 10 deletions

View File

@ -44,7 +44,7 @@ In general, functional types conversion could be lazy or eager, be performed in
```scala
import cats.Id
val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.runF[Id](33)
val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.direct.runF[Id](33)
```
@ -121,7 +121,8 @@ libraryDependencies ++= Seq(
// eyJpZCI6MjM0LCJuYW1lIjoiSGV5IEJvYiJ9
```
For more real-world examples, see [Fluence](https://github.com/fluencelabs/fluence).
For synthetic examples refer to the [examples directory](examples/).
For the real-world examples checkout [Fluence](https://github.com/fluencelabs/fluence) main repo.
## Roadmap

View File

@ -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",
@ -148,3 +148,14 @@ lazy val `codec-protobuf` = crossProject(JVMPlatform, JSPlatform)
lazy val `codec-protobuf-jvm` = `codec-protobuf`.jvm
lazy val `codec-protobuf-js` = `codec-protobuf`.js
lazy val `codec-examples` = project
.in(file("examples"))
.settings(
commons,
libraryDependencies ++= Seq(
"io.monix" %%% "monix" % "3.0.0-RC1"
)
)
.dependsOn(`codec-core-jvm`)
.dependsOn(`codec-kryo`)

View File

@ -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.
*/

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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]
)
}

View File

@ -0,0 +1,65 @@
package fluence.codec.examples
import cats.Id
import fluence.codec.kryo.KryoCodecs
import fluence.codec.{CodecError, PureCodec}
import monix.eval.Task
import shapeless.{::, HNil}
object KryoCodecExample {
case class Aircraft(manufacturer: String, model: String, tailNumber: String)
case class Fuel(amount: Double) extends AnyVal
case class UnknownClass(x: String)
def main(args: Array[String]): Unit = {
// This way we can define a typed collection of codecs using kryo for the underlying serialization.
//
// These codecs can be used only to transform the corresponding type: i.e. it won't be possible to
// use an aircraft codec to serialize fuel (which is essentially a typed wrapper over double value).
//
// It won't be possible to obtain from this collection a codec for previously not registered class.
// Type safety FTW!
//
// Note that different methods are used to register Aircraft and Fuel that's because one is a reference,
// and another is a value type.
val codecs: KryoCodecs[Task, ::[Fuel, ::[Aircraft, ::[Array[Byte], ::[Long, ::[String, HNil]]]]]] = KryoCodecs()
.addCase(classOf[Aircraft])
.add[Fuel]
.build[Task]()
val skyhawk61942 = Aircraft("Cessna", "172S G1000", "N61942")
val tabsFuel = Fuel(53)
val aircraftCodec: PureCodec[Aircraft, Array[Byte]] = codecs.pureCodec[Aircraft]
val fuelCodec: PureCodec[Fuel, Array[Byte]] = codecs.pureCodec[Fuel]
// This will cause a compilation error, because the class was never registered with the codecs.
// "You requested an element of type (...).UnknownClass, but there is none in the HList"
//
// val unknownCodec = codecs.pureCodec[UnknownClass]
// Here all the standard machinery of codecs applies (for more examples, consider checking out PureCodecExample.
// We can serialize and deserialize the object and unsurprisingly the original and restored values match.
//
// Let's serialize an aircraft instance.
{
val ser: Id[Either[CodecError, Array[Byte]]] = aircraftCodec.direct[Id](skyhawk61942).value
val deser: Id[Either[CodecError, Aircraft]] = aircraftCodec.inverse[Id](ser.right.get).value
println(ser.right.map(x => s"$skyhawk61942 => serialized size: ${x.length}"))
assert(deser == Right(skyhawk61942))
}
// Same thing for the fuel instance (which is AnyVal fwiw).
{
val ser: Id[Either[CodecError, Array[Byte]]] = fuelCodec.direct[Id](tabsFuel).value
val deser: Id[Either[CodecError, Fuel]] = fuelCodec.inverse[Id](ser.right.get).value
println(ser.right.map(x => s"$tabsFuel => serialized size: ${x.length}"))
assert(deser == Right(tabsFuel))
}
}
}

View File

@ -0,0 +1,138 @@
package fluence.codec.examples
import cats.Id
import cats.data.EitherT
import cats.implicits._
import fluence.codec.PureCodec.{Bijection, Point}
import fluence.codec.{CodecError, PureCodec}
import scala.util.Try
object PureCodecExample {
def main(args: Array[String]): Unit = {
// Here we are defining a simple codec transforming a string to integer and back.
//
// It's not really a bijection: even not taking into account unparseable strings like "test", there are
// different string values (e.g., "+20" and "20") producing the same integer value. It's good enough for
// demonstration purposes though, so we keep using it.
val str2intCodec: Bijection[String, Int] = PureCodec.build[String, Int](
(x: String) => x.toInt,
(x: Int) => x.toString
)
// Using an identity monad, we can parse a valid string into integer (which produces EitherT) and then map
// the result. Now, we can use EitherT[F, E, B] or convert it into F[Either[E, B]] representation.
{
val res: EitherT[Id, CodecError, Int] = str2intCodec.direct[Id]("31330").map(_ + 7)
val resMonad: Id[Either[CodecError, Int]] = res.value
assert(res.toString == "EitherT(Right(31337))")
assert(resMonad.toString == "Right(31337)")
}
// We can also supply a different type class (Monad[F[_]]) in this case the result will be wrapped into
// the corresponding type F[_] using the `F.pure(_)` method.
{
val res = str2intCodec.direct[Option]("42")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Right(42)))")
assert(resMonad.toString == "Some(Right(42))")
}
// Here we attempt to pass an unparseable string value. Note that PureCodec won't catch a thrown exception
// automatically despite that return type is EitherT (this might be a bit confusing). Instead, the exception
// will come all the way up to the caller, which will have to handle it manually.
{
val resWrapped = Try {
val res: EitherT[Id, CodecError, Int] = str2intCodec.direct[Id]("foo")
res
}
assert(resWrapped.toString == "Failure(java.lang.NumberFormatException: For input string: \"foo\")")
}
// To handle exceptions automatically, we can use Try monad. Note that we get `EitherT(Failure(...))`, not
// `EitherT(Failure(Right(...)))` as one might expect by analogy with previous examples. It's not
// `EitherT(Left(...))` too which could have been more convenient potentially.
{
val res = str2intCodec.direct[Try]("foo")
val resMonad: Try[Either[CodecError, Int]] = res.value
assert(res.toString == "EitherT(Failure(java.lang.NumberFormatException: For input string: \"foo\"))")
assert(resMonad.toString == "Failure(java.lang.NumberFormatException: For input string: \"foo\")")
}
// If we really want to receive Left with the exception info when the string argument can't be parsed, a little
// more effort is needed. The problem we had before was that the supplied function `(x: String) => x.toInt`
// could throw parse exceptions and therefore was not really pure.
//
// However, we can catch exceptions in this function and return an Either, which will make it pure. Now, all we
// need to do is to lift this function into the Func context.
val str2intEitherCodec: Bijection[String, Int] = PureCodec.build(
PureCodec.liftFuncEither((x: String) => Either.catchNonFatal(x.toInt).left.map(e => CodecError(e.getMessage))),
PureCodec.liftFuncEither((x: Int) => Either.catchNonFatal(x.toString).left.map(e => CodecError(e.getMessage)))
)
// For lawful strings those which can be parsed into an integer the behavior hasn't really changed.
// Note that we receive Right(...) wrapped in the supplied monadic type.
{
val res: EitherT[Option, CodecError, Int] = str2intEitherCodec.direct[Option]("1024")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Right(1024)))")
assert(resMonad.toString == "Some(Right(1024))")
}
// However, for strings that can't be parsed, we will receive Left(...) which is a desired behavior!
{
val res: EitherT[Option, CodecError, Int] = str2intEitherCodec.direct[Option]("bar")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Left(fluence.codec.CodecError: For input string: \"bar\")))")
assert(resMonad.toString == "Some(Left(fluence.codec.CodecError: For input string: \"bar\"))")
}
// It's also totally possible to perform an inverse transformation: after all, a codec is a bijection.
{
val res: EitherT[Id, CodecError, String] = str2intCodec.inverse[Id](720)
val resMonad: Id[Either[CodecError, String]] = res.value
assert(res.toString == "EitherT(Right(720))")
assert(resMonad.toString == "Right(720)")
}
// It's also possible to pass the to-be-converted value first, but perform the actual transformation only
// later on (using different enclosing monads if desired). To achieve this, `pointAt` method which returns a
// lazily evaluated function can be used.
{
val point: Point[Int] = str2intCodec.direct.pointAt("333")
val resId: EitherT[Id, CodecError, Int] = point[Id]()
val resOption: EitherT[Option, CodecError, Int] = point[Option]()
assert(resId.toString == "EitherT(Right(333))")
assert(resOption.toString == "EitherT(Some(Right(333)))")
}
// Sometimes, we might want to be able to compose two codecs together. Here we define an integer to boolean
// codec and compose it with one of the previously defined codecs. Yes, the int-to-bool codec is not really
// a bijection but we can put up with that for the sake of example.
val int2boolCodec: Bijection[Int, Boolean] = PureCodec.build[Int, Boolean](
(x: Int) => x != 0,
(x: Boolean) => if (x) 1 else 0
)
val str2boolCodec: Bijection[String, Boolean] = str2intCodec andThen int2boolCodec
{
val resA: EitherT[Id, CodecError, Boolean] = str2boolCodec.direct[Id]("100")
val resB = str2boolCodec.inverse[Option](true)
assert(resA.toString == "EitherT(Right(true))")
assert(resB.toString == "EitherT(Some(Right(1)))")
}
// TODO: describe `runF` and `toKleisli`
}
}

View File

@ -21,8 +21,8 @@ import com.twitter.chill.{AllScalaRegistrar, KryoBase, KryoInstantiator}
import org.objenesis.strategy.StdInstantiatorStrategy
/**
* This Instantiator enable compulsory class registration, registers all java and scala main classes.
* This class required for [[com.twitter.chill.KryoPool]].
* This Instantiator enables compulsory class registration and registers all java and scala main classes.
* This class is required for [[com.twitter.chill.KryoPool]].
* @param classesToReg additional classes for registration
* @param registrationRequired if true, an exception is thrown when an unregistered class is encountered.
*/