mirror of
https://github.com/fluencelabs/codec
synced 2025-07-01 23:41:36 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
8d905117c8 | |||
60661d5555 | |||
e4ab5f147f | |||
f23f9dcfab | |||
6efaac148b | |||
6378af3658 | |||
e1b5bfb947 |
@ -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
|
||||
|
||||
|
13
build.sbt
13
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",
|
||||
@ -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`)
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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`
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
Reference in New Issue
Block a user