17 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
963ae412a7 Merge pull request #2 from fluencelabs/bj-category-with-split
Bijection is a Category with split
2018-05-16 16:13:23 +03:00
f8e22773d7 Scalafmt formatting 2018-05-16 15:50:19 +03:00
465a64f341 Bijection is a Category with split 2018-05-16 15:47:00 +03:00
b04c4077f9 Gitter badge 2018-05-16 12:32:32 +03:00
64b6989027 Merge pull request #1 from fluencelabs/readme
Readme
2018-04-27 09:35:51 +03:00
4f7b44847c Travis build badge 2018-04-26 10:44:33 +03:00
9818275ed3 scalafmt on build.sbt 2018-04-25 17:42:29 +03:00
9573c0d485 Unnecessary dependency removed from build.sbt 2018-04-25 17:07:17 +03:00
9210182cda Better Readme 2018-04-25 17:00:11 +03:00
31b212e1e1 Readme 2018-04-23 14:50:10 +03:00
11 changed files with 526 additions and 30 deletions

138
README.md
View File

@ -1,3 +1,139 @@
[![Build Status](https://travis-ci.org/fluencelabs/codec.svg?branch=master)](https://travis-ci.org/fluencelabs/codec) [![Gitter](https://badges.gitter.im/fluencelabs/codec.svg)](https://gitter.im/fluencelabs/codec?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
# Codec
Like bijections, but pure and suitable for `cats`.
`Codec` is an opinionated library for `cats`-friendly pure (reversible) conversions between types, with possible errors represented with `Either`.
It's cross-built for Scala and Scala.js, and can also be used for other free-of-effect conversions to reach functional composability.
## Motivation
### Use partial functions, track the errors
Often it's useful to do some validation alongside conversion, like in `String => Int`.
However, this function throws when malformed input is given. Hence `String => Either[Throwable, Int]`, being a total function, should fit better.
In this case, error is reflected in type system. It keeps things pure. We go further, forcing the error to be of type `CodecError`, so that later it's easy to track where it comes from, especially in asynchronous environment.
This uni-direction type is called `fluence.codec.PureCodec.Func` for a fixed `CodecError` error type. Any other error type could be used by extending `fluence.codec.MonadicalEitherArrow[Error]`.
Bidirection type `A <=> B` is composed from two `Func`s and is called `Bijection`.
### Lawful composition
A type `Func[A, B]`, being something like `A => Either[E, B]`, is not very composable on it's own, so we implemented `cats.arrow.ArrowChoice[Func]` for it. You may use `cats.syntax.compose._` or anything like that to receive `andThen` and other lawful functions.
`Bijection[A, B]` is more complex type, so it has only `Compose[Bijection]` typeclass. Finally you can do something like this:
```scala
import cats.syntax.compose._
import fluence.codec.PureCodec
val intToBytes: PureCodec[Int, Array[Byte]] = PureCodec[Int, String] andThen PureCodec[String, Array[Byte]]
```
Errors are handled in monad-like "fail-fast" fashion.
### Benefit from different Monads
In general, functional types conversion could be lazy or eager, be performed in current thread or another. This choice should not affect the logic of conversion, as it's pure.
`PureCodec` may use any monad to preform execution upon, retaining its nature. The most simple case is strict eager evaluation:
```scala
import cats.Id
val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.direct.runF[Id](33)
```
You may use any monad, like `Task`, `Coeval`, `Eval`, `IO`...
### Minimal dependencies
`codec-core` depends only on `cats`. Each particular codec set is moved into separate module.
### Cross compile
In case of complex algorithms, it's worthy to share codebase between platforms. We cross-compile all the codecs possible both to Scala and Scala.js.
## Installation
```scala
// Bintray repo is used so far. Migration to Maven Central is planned
resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
val codecV = "0.0.1"
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-core" % codecV, // basic types
"one.fluence" %%% "codec-bits" % codecV, // scodec-bits conversions for ByteVector
"one.fluence" %%% "codec-circe" % codecV, // json handling with circe
"one.fluence" %%% "codec-protobuf" % codecV, // ByteString conversions for both scala and scala.js
"one.fluence" %% "codec-kryo" % codecV // typesafe kryo codecs, only for scala
)
```
## Example
```scala
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.codec.circe.CirceCodecs._
import io.circe.{Decoder, Encoder, Json}
import scodec.bits.ByteVector
import fluence.codec.bits.BitsCodecs._
// Simple class
case class User(id: Int, name: String)
// Encode and decode with circe
implicit val encoder: Encoder[User] =
user Json.obj("id" Encoder.encodeInt(user.id), "name" Encoder.encodeString(user.name))
implicit val decoder: Decoder[User] = cursor
for {
id cursor.downField("id").as[Int]
name cursor.downField("name").as[String]
} yield User(id, name)
// Get codec for encoder/decoder
implicit val userJson: PureCodec[User, Json] = circeJsonCodec(encoder, decoder)
// A trivial string to bytes codec; never use it in production!
implicit val stringCodec: PureCodec[String, Array[Byte]] =
PureCodec.liftB(_.getBytes, bs new String(bs))
// Convert user to byte vector and vice versa
implicit val userJsonVec: PureCodec[User, ByteVector] =
PureCodec[User, Json] andThen
PureCodec[Json, String] andThen
PureCodec[String, Array[Byte]] andThen
PureCodec[Array[Byte], ByteVector]
// Try it with an instance
val user = User(234, "Hey Bob")
// unsafe() is to be used in tests only; it throws!
println(userJsonVec.direct.unsafe(user).toBase64)
// eyJpZCI6MjM0LCJuYW1lIjoiSGV5IEJvYiJ9
```
For synthetic examples refer to the [examples directory](examples/).
For the real-world examples checkout [Fluence](https://github.com/fluencelabs/fluence) main repo.
## Roadmap
- `connect[A, B, C]` to compose several Funcs or Bijections
- `sbt-tut` for docs
- Implement more codecs
- Enhance `Func` api with shortcuts to `EitherT` methods
- Consider improving performance: `EitherT` [is not so fast](https://twitter.com/alexelcu/status/988031831357485056) (at least yet)
## License
Fluence is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3 (AGPLv3) as published by the Free Software Foundation.
Fluence includes some [external modules](https://github.com/fluencelabs/codec/blob/master/build.sbt) that carry their own licensing.

View File

@ -14,7 +14,7 @@ val scalaV = scalaVersion := "2.12.5"
val commons = Seq(
scalaV,
version := "0.0.1",
version := "0.0.3",
fork in Test := true,
parallelExecution in Test := false,
organization := "one.fluence",
@ -22,10 +22,10 @@ val commons = Seq(
organizationHomepage := Some(new URL("https://fluence.one")),
startYear := Some(2017),
licenses += ("AGPL-V3", new URL("http://www.gnu.org/licenses/agpl-3.0.en.html")),
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
bintrayOrganization := Some("fluencelabs"),
publishMavenStyle := true,
bintrayRepository := "releases"
publishMavenStyle := true,
bintrayRepository := "releases"
)
commons
@ -61,12 +61,12 @@ lazy val `codec-core` = crossProject(JVMPlatform, JSPlatform)
commons,
kindProjector,
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % Cats1V,
"org.typelevel" %%% "cats-laws" % Cats1V % Test,
"org.typelevel" %%% "cats-testkit" % Cats1V % Test,
"org.typelevel" %%% "cats-core" % Cats1V,
"org.typelevel" %%% "cats-laws" % Cats1V % Test,
"org.typelevel" %%% "cats-testkit" % Cats1V % Test,
"com.github.alexarchambault" %%% "scalacheck-shapeless_1.13" % "1.1.8" % Test,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
@ -84,9 +84,9 @@ lazy val `codec-bits` = crossProject(JVMPlatform, JSPlatform)
.settings(
commons,
libraryDependencies ++= Seq(
"org.scodec" %%% "scodec-bits" % ScodecBitsV,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
"org.scodec" %%% "scodec-bits" % ScodecBitsV,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
@ -125,8 +125,8 @@ lazy val `codec-kryo` = project
commons,
libraryDependencies ++= Seq(
chill,
"com.chuusai" %% "shapeless" % ShapelessV,
"org.scalatest" %% "scalatest" % ScalatestV % Test
"com.chuusai" %% "shapeless" % ShapelessV,
"org.scalatest" %% "scalatest" % ScalatestV % Test
)
)
.dependsOn(`codec-core-jvm`)
@ -149,3 +149,13 @@ 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

@ -22,7 +22,7 @@ import io.circe._
object CirceCodecs {
implicit def circeJsonCodec[T](encoder: Encoder[T], decoder: Decoder[T]): PureCodec[T, Json] =
implicit def circeJsonCodec[T](implicit encoder: Encoder[T], decoder: Decoder[T]): PureCodec[T, Json] =
PureCodec.liftEitherB[T, Json](
t Right(encoder.apply(t)),
json decoder.decodeJson(json).left.map(f CodecError("Cannot decode json value", causedBy = Some(f)))

View File

@ -18,12 +18,13 @@
package fluence.codec
import cats.{Monad, MonadError, Traverse}
import cats.arrow.{ArrowChoice, Compose}
import cats.arrow.{ArrowChoice, Category}
import cats.syntax.arrow._
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
/**
@ -32,6 +33,7 @@ import scala.util.Try
* @tparam E Error type
*/
abstract class MonadicalEitherArrow[E <: Throwable] {
mea
/**
* Alias for error type
@ -60,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)
/**
@ -76,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))
/**
@ -91,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)
}
/**
@ -114,6 +127,13 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
)
def toCodec[F[_]](implicit F: MonadError[F, Throwable]): Codec[F, A, B] =
Codec(direct.runF[F], inverse.runF[F])
/**
* Splits the input and puts it to either bijection, then merges output.
* It could have been achieved with `Strong` typeclass in case it doesn't extend `Profunctor`; but it does.
*/
def split[A1, B1](bj: Bijection[A1, B1]): Bijection[(A, A1), (B, B1)] =
mea.split(this, bj)
}
/**
@ -173,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.
*/
@ -206,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.
*
@ -228,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.
*/
@ -250,10 +332,20 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
implicit def swap[A, B](implicit bijection: Bijection[A, B]): Bijection[B, A] = bijection.swap
/**
* Bijection should obey ComposeLaws
* Bijection should obey CategoryLaws
*/
implicit object catsMonadicalBijectionCompose extends Compose[Bijection] {
implicit object catsMonadicalBijectionCategory extends Category[Bijection] {
override def compose[A, B, C](f: Bijection[B, C], g: Bijection[A, B]): Bijection[A, C] =
Bijection(f.direct compose g.direct, g.inverse compose f.inverse)
override def id[A]: Bijection[A, A] = identityBijection
}
/**
* Splits the input and puts it to either bijection, then merges output.
* It could be achieved with `Strong` typeclass in case it doesn't extend `Profunctor`; but it does.
*/
def split[A1, B1, A2, B2](f1: Bijection[A1, B1], f2: Bijection[A2, B2]): Bijection[(A1, A2), (B1, B2)] =
Bijection(f1.direct *** f2.direct, f1.inverse *** f2.inverse)
}

View File

@ -17,7 +17,7 @@
package fluence.codec
import cats.laws.discipline.ComposeTests
import cats.laws.discipline.CategoryTests
import cats.tests.CatsSuite
import cats.Eq
import org.scalacheck.ScalacheckShapeless._
@ -32,7 +32,7 @@ class PureCodecBijectionLawsSpec extends CatsSuite {
Eq.instance((x, y) directEq.eqv(x.direct, y.direct) && inverseEq.eqv(x.inverse, y.inverse))
checkAll(
"PureCodec.Bijection.ComposeLaws",
ComposeTests[PureCodec].compose[Int, String, Double, BigDecimal]
"PureCodec.Bijection.CategoryLaws",
CategoryTests[PureCodec].category[Int, String, Double, BigDecimal]
)
}

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

View File

@ -1 +1 @@
sbt.version = 1.1.2
sbt.version = 1.1.4