13 Commits
0.0.1 ... 0.0.3

Author SHA1 Message Date
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
8 changed files with 310 additions and 29 deletions

137
README.md
View File

@ -1,3 +1,138 @@
[![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.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 more real-world examples, see [Fluence](https://github.com/fluencelabs/fluence).
## 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`)
@ -148,4 +148,3 @@ lazy val `codec-protobuf` = crossProject(JVMPlatform, JSPlatform)
lazy val `codec-protobuf-jvm` = `codec-protobuf`.jvm
lazy val `codec-protobuf-js` = `codec-protobuf`.js

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

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