Skip to content
This repository was archived by the owner on Jul 10, 2025. It is now read-only.

[WIP] Added PureCodec examples #9

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

```

Expand Down Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Original file line number Diff line number Diff line change
Expand Up @@ -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](())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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]()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how this Task affects things later on.
Doesn't look like it's going to be pushed as a monad into the codecs being built...


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))
}
}
}
138 changes: 138 additions & 0 deletions examples/src/main/scala/fluence/codec/examples/PureCodecExample.scala
Original file line number Diff line number Diff line change
@@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I'm not sure why this happens and it's pretty confusing to say the least.

// `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`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it seems the runF example quoted on the readme page is off. When I've tried to use runF accordingly to it, the compilation error has occurred. The code was:

val x = str2intCodec.direct.runF[Id]("foo")
println(x)

The error was:

[error] /Users/xdralex/Work/fluence/codec/examples/src/main/scala/fluence/codec/examples/PureCodecExample.scala:55:41: could not find implicit value for parameter F: cats.MonadError[cats.Id, _ >: fluence.codec.CodecError]
[error]     val x = str2intCodec.direct.runF[Id]("foo")
[error]                                         ^
[error] one error found

Not sure how the correct usage pattern should look like.

}
}
4 changes: 2 additions & 2 deletions kryo/src/main/scala/fluence/codec/kryo/KryoFactory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down