diff --git a/README.md b/README.md index 16e0ebb..86d0cfa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.sbt b/build.sbt index 931c2e9..77da212 100644 --- a/build.sbt +++ b/build.sbt @@ -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`) diff --git a/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala b/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala index 4c8cfc5..0a11c09 100644 --- a/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala +++ b/core/src/main/scala/fluence/codec/MonadicalEitherArrow.scala @@ -208,7 +208,7 @@ abstract class MonadicalEitherArrow[E <: Throwable] { * @param f Function to lift */ def liftFuncPoint[A, B](f: A ⇒ Point[B]): Func[A,B] = - new Func[A,B]{ + new Func[A, B]{ override def apply[F[_] : Monad](input: A): EitherT[F, E, B] = f(input).apply[F](()) } diff --git a/examples/src/main/scala/fluence/codec/examples/KryoCodecExample.scala b/examples/src/main/scala/fluence/codec/examples/KryoCodecExample.scala new file mode 100644 index 0000000..662c670 --- /dev/null +++ b/examples/src/main/scala/fluence/codec/examples/KryoCodecExample.scala @@ -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)) + } + } +} diff --git a/examples/src/main/scala/fluence/codec/examples/PureCodecExample.scala b/examples/src/main/scala/fluence/codec/examples/PureCodecExample.scala new file mode 100644 index 0000000..d7c80fb --- /dev/null +++ b/examples/src/main/scala/fluence/codec/examples/PureCodecExample.scala @@ -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` + } +} diff --git a/kryo/src/main/scala/fluence/codec/kryo/KryoFactory.scala b/kryo/src/main/scala/fluence/codec/kryo/KryoFactory.scala index 74e92ed..d17c2d8 100644 --- a/kryo/src/main/scala/fluence/codec/kryo/KryoFactory.scala +++ b/kryo/src/main/scala/fluence/codec/kryo/KryoFactory.scala @@ -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. */