Compare commits

...

23 Commits

Author SHA1 Message Date
alari
cf36fdb00e A couple of shortcut methods on Func 2019-08-14 16:33:25 +03:00
Dmitry Kurinskiy
70dae6edca
Merge pull request #13 from fluencelabs/pump-dependencies
deps updated
2019-04-17 11:32:52 +03:00
alari
3c4a7fc722 deps updated 2019-04-16 19:50:20 +03:00
Dmitry Kurinskiy
e0d046b197
Merge pull request #12 from fluencelabs/update-deps-0.0.4
Updating dependencies
2019-01-23 11:48:32 +03:00
alari
b8b07d2a8d Newline 2019-01-23 11:21:39 +03:00
alari
d23e60f516 Kryo tests fixed 2019-01-22 18:14:20 +03:00
alari
7dee94d09b Gitter link removed, as we don't use it 2019-01-22 18:05:21 +03:00
alari
744e5b92b8 Tests compilation fixed 2019-01-22 17:57:08 +03:00
alari
cfd132db76 Tests compilation fixed 2019-01-21 19:29:24 +03:00
alari
a3de737d45 Updating dependencies 2019-01-21 18:27:43 +03:00
Dmitry Kurinskiy
6efaac148b
Merge pull request #3 from fluencelabs/point
Introducing Point monad
2018-05-21 13:54:20 +03:00
alari
6378af3658 Docs for existential type bound for MonadError 2018-05-21 13:45:19 +03:00
alari
e1b5bfb947 Introducing Point monad 2018-05-21 11:59:37 +03:00
Dmitry Kurinskiy
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
alari
f8e22773d7 Scalafmt formatting 2018-05-16 15:50:19 +03:00
alari
465a64f341 Bijection is a Category with split 2018-05-16 15:47:00 +03:00
Dmitry Kurinskiy
b04c4077f9
Gitter badge 2018-05-16 12:32:32 +03:00
Dmitry Kurinskiy
64b6989027
Merge pull request #1 from fluencelabs/readme
Readme
2018-04-27 09:35:51 +03:00
alari
4f7b44847c Travis build badge 2018-04-26 10:44:33 +03:00
alari
9818275ed3 scalafmt on build.sbt 2018-04-25 17:42:29 +03:00
alari
9573c0d485 Unnecessary dependency removed from build.sbt 2018-04-25 17:07:17 +03:00
alari
9210182cda Better Readme 2018-04-25 17:00:11 +03:00
alari
31b212e1e1 Readme 2018-04-23 14:50:10 +03:00
15 changed files with 408 additions and 188 deletions

View File

@ -2,7 +2,7 @@ sudo: required
language: scala
scala:
- 2.12.5
- 2.12.8
jdk:
- oraclejdk8

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)
# 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.4"
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

@ -29,18 +29,36 @@ object BitsCodecs {
implicit val byteArrayToVector: PureCodec[Array[Byte], ByteVector] =
liftB(ByteVector.apply, _.toArray)
// Notice the use of default Base64 alphabet
implicit val base64ToVector: PureCodec[String, ByteVector] =
base64AlphabetToVector(Bases.Alphabets.Base64)
object Base64 {
// Notice the use of default Base64 alphabet
implicit val base64ToVector: PureCodec[String, ByteVector] =
alphabetToVector(Bases.Alphabets.Base64)
def base64AlphabetToVector(alphabet: Bases.Base64Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase64Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase64(alphabet))
)
def alphabetToVector(alphabet: Bases.Base64Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase64Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase64(alphabet))
)
}
object Base58 {
// Notice the use of default Base64 alphabet
implicit val base58ToVector: PureCodec[String, ByteVector] =
alphabetToVector(Bases.Alphabets.Base58)
def alphabetToVector(alphabet: Bases.Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase58Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase58(alphabet))
)
}
}

View File

@ -22,6 +22,7 @@ import cats.syntax.compose._
import org.scalatest.prop.Checkers
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import BitsCodecs.Base64._
import BitsCodecs._
import fluence.codec.PureCodec
@ -32,7 +33,7 @@ class BitsCodecsSpec extends WordSpec with Matchers with Checkers {
val arrCodec = implicitly[PureCodec[Array[Byte], ByteVector]]
val b64Codec = implicitly[PureCodec[ByteVector, String]]
check { (bytes: List[Byte])
check { bytes: List[Byte]
(arrCodec andThen arrCodec.swap).direct.apply[Id](bytes.toArray).value.map(_.toList).contains(bytes) &&
(arrCodec andThen b64Codec andThen b64Codec.swap andThen arrCodec.swap).direct
.apply[Id](bytes.toArray)

View File

@ -1,6 +1,6 @@
import de.heikoseeberger.sbtheader.License
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import sbtcrossproject.crossProject
import sbtcrossproject.CrossPlugin.autoImport.crossProject
name := "codec"
@ -10,11 +10,12 @@ javaOptions in Test ++= Seq("-ea")
skip in publish := true // Skip root project
val scalaV = scalaVersion := "2.12.5"
val scalaV = scalaVersion := "2.12.8"
val commons = Seq(
scalaV,
version := "0.0.1",
//crossScalaVersions := Seq(scalaVersion.value, "2.13.0-RC1"),
version := "0.0.5",
fork in Test := true,
parallelExecution in Test := false,
organization := "one.fluence",
@ -22,25 +23,27 @@ 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
val kindProjector = addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.6")
val kindProjector = addCompilerPlugin("org.typelevel" % "kind-projector" % "0.10.0" cross CrossVersion.binary)
val Cats1V = "1.1.0"
val ScodecBitsV = "1.1.5"
val CirceV = "0.9.3"
val Cats1V = "1.6.0"
val ScodecBitsV = "1.1.10"
val CirceV = "0.11.1"
val ShapelessV = "2.3.+"
val chill = "com.twitter" %% "chill" % "0.9.2"
val chill = "com.twitter" %% "chill" % "0.9.3"
val ScalatestV = "3.0.+"
val ScalacheckV = "1.13.4"
val ScalatestV = "3.0.5"
// Note that cats-laws 1.5 are compiled against scalacheck 1.13, and scalacheck-shapeless should also not introduce the upgrade
val ScalacheckV = "1.13.5"
val protobuf = Seq(
PB.targets in Compile := Seq(
@ -61,12 +64,11 @@ 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-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 +86,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 +127,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 +150,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

@ -1,97 +0,0 @@
/*
* 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.data.Kleisli
import cats.{Applicative, FlatMap, Traverse}
import cats.syntax.applicative._
import scala.language.{higherKinds, implicitConversions}
/**
* Base trait for serialize/deserialize objects.
*
* @tparam A The type of plain object representation
* @tparam B The type of binary representation
* @tparam F Encoding/decoding effect
*/
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
final case class Codec[F[_], A, B](encode: A F[B], decode: B F[A]) {
self
implicit val direct: Kleisli[F, A, B] = Kleisli(encode)
implicit val inverse: Kleisli[F, B, A] = Kleisli(decode)
def andThen[C](other: Codec[F, B, C])(implicit F: FlatMap[F]): Codec[F, A, C] =
Codec((self.direct andThen other.direct).run, (other.inverse andThen self.inverse).run)
def compose[C](other: Codec[F, C, A])(implicit F: FlatMap[F]): Codec[F, C, B] =
Codec((other.direct andThen self.direct).run, (self.inverse andThen other.inverse).run)
def swap: Codec[F, B, A] = Codec(decode, encode)
}
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
object Codec {
implicit def identityCodec[F[_]: Applicative, T]: Codec[F, T, T] =
Codec(_.pure[F], _.pure[F])
implicit def traverseCodec[F[_]: Applicative, G[_]: Traverse, O, B](
implicit codec: Codec[F, O, B]
): Codec[F, G[O], G[B]] =
Codec[F, G[O], G[B]](Traverse[G].traverse[F, O, B](_)(codec.encode), Traverse[G].traverse[F, B, O](_)(codec.decode))
implicit def toDirect[F[_], A, B](implicit cod: Codec[F, A, B]): Kleisli[F, A, B] =
cod.direct
implicit def toInverse[F[_], A, B](implicit cod: Codec[F, A, B]): Kleisli[F, B, A] =
cod.inverse
implicit def swap[F[_], A, B](implicit cod: Codec[F, A, B]): Codec[F, B, A] =
Codec[F, B, A](cod.decode, cod.encode)
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
def codec[F[_], O, B](implicit codec: Codec[F, O, B]): Codec[F, O, B] = codec
/**
* Constructs a Codec from pure encode/decode functions and an Applicative
*
* @param encodeFn Encode function that never fail
* @param decodeFn Decode function that never fail
* @tparam F Applicative effect
* @tparam O Raw type
* @tparam B Encoded type
* @return New codec for O and B
*/
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
def pure[F[_]: Applicative, O, B](encodeFn: O B, decodeFn: B O): Codec[F, O, B] =
Codec(encodeFn(_).pure[F], decodeFn(_).pure[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
@ -47,6 +49,7 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
* @tparam B Successful result type
*/
abstract class Func[A, B] {
f
/**
* Run the func on input, using the given monad.
@ -60,9 +63,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)
/**
@ -73,12 +77,37 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
*/
def apply[F[_]: Monad](input: A): EitherT[F, E, B]
/**
* Shortcut for function composition
*
* @param other Other function to run after
* @tparam C Resulting input type
* @return Composed function
*/
def on[C](other: Func[C, A]): Func[C, B] =
catsMonadicalEitherArrowChoice.compose(this, other)
/**
* Convert this Func into another one, lifting the error
*
* @param m Another instance of MonadicalEitherArrow
* @param convertE Convert error
* @tparam EE Error type
* @return Converted function
*/
def to[EE <: Throwable](m: MonadicalEitherArrow[EE])(implicit convertE: E EE): m.Func[A, B] =
new m.Func[A, B] {
override def apply[F[_]: Monad](input: A): EitherT[F, EE, B] =
f[F](input).leftMap(convertE)
}
/**
* 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 +120,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)
}
/**
@ -108,12 +146,12 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
*/
lazy val swap: Bijection[B, A] = Bijection(inverse, direct)
@deprecated(
"You should keep codec Pure until running direct or inverse on it: there's no reason to bind effect into Codec",
"6.4.2018"
)
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 +211,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 +264,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 +322,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 +350,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,52 @@
/*
* 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.{Applicative, Eq, Invariant}
import cats.data.EitherT
import cats.syntax.functor._
import cats.laws.discipline.{MonadErrorTests, SemigroupalTests}
import cats.tests.CatsSuite
import fluence.codec
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary._
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

@ -17,36 +17,29 @@
package fluence.codec.kryo
import cats.MonadError
import com.twitter.chill.KryoPool
import fluence.codec.{Codec, CodecError, PureCodec}
import fluence.codec.{CodecError, PureCodec}
import shapeless._
import scala.language.higherKinds
import scala.reflect.ClassTag
import scala.util.Try
import scala.util.control.NonFatal
/**
* Wrapper for a KryoPool with a list of registered classes
*
* @param pool Pre-configured KryoPool
* @param F Applicative error
* @tparam L List of classes registered with kryo
* @tparam F Effect
*/
class KryoCodecs[F[_], L <: HList] private (pool: KryoPool)(implicit F: MonadError[F, Throwable]) {
class KryoCodecs[L <: HList] private (pool: KryoPool) {
/**
* Returns a codec for any registered type
*
* @param sel Shows the presence of type T within list L
* @tparam T Object type
* @return Freshly created Codec with Kryo inside
* @return Freshly created PureCodec with Kryo inside
*/
implicit def codec[T](implicit sel: ops.hlist.Selector[L, T]): Codec[F, T, Array[Byte]] =
pureCodec[T].toCodec[F]
implicit def pureCodec[T](implicit sel: ops.hlist.Selector[L, T]): PureCodec[T, Array[Byte]] =
PureCodec.Bijection(
PureCodec.liftFuncEither { input
@ -112,10 +105,10 @@ object KryoCodecs {
* @tparam F Effect type
* @return Configured instance of KryoCodecs
*/
def build[F[_]](
def build(
poolSize: Int = Runtime.getRuntime.availableProcessors
)(implicit F: MonadError[F, Throwable]): KryoCodecs[F, L] =
new KryoCodecs[F, L](
): KryoCodecs[L] =
new KryoCodecs[L](
KryoPool.withByteArrayOutputStream(
poolSize,
KryoFactory(klasses, registrationRequired = true) // registrationRequired should never be needed, as codec derivation is typesafe

View File

@ -18,6 +18,7 @@
package fluence.codec.kryo
import cats.instances.try_._
import cats.syntax.profunctor._
import org.scalatest.{Matchers, WordSpec}
import scala.util.Try
@ -31,15 +32,15 @@ class KryoCodecsSpec extends WordSpec with Matchers {
KryoCodecs()
.add[Array[Array[Byte]]]
.addCase(classOf[TestClass])
.build[Try]()
.build()
"encode and decode" should {
"be inverse functions" when {
"object defined" in {
val codec = testCodecs.codec[TestClass]
val codec = testCodecs.pureCodec[TestClass]
val result = codec.encode(testClass).flatMap(codec.decode).get
val result = codec.inverse.unsafe(codec.direct.unsafe(testClass))
result.str shouldBe "one"
result.num shouldBe 2
@ -47,8 +48,8 @@ class KryoCodecsSpec extends WordSpec with Matchers {
}
"object is null" in {
val codec = testCodecs.codec[TestClass]
val result = codec.encode(null).flatMap(codec.decode)
val codec = testCodecs.pureCodec[TestClass]
val result = codec.direct.runF[Try](null).flatMap(codec.inverse.runF[Try])
result.isFailure shouldBe true
}
}
@ -57,9 +58,8 @@ class KryoCodecsSpec extends WordSpec with Matchers {
"encode" should {
"not write full class name to binary representation" when {
"class registered" in {
//val codec = KryoCodec[TestClass](Seq(classOf[TestClass], classOf[Array[Byte]], classOf[Array[Array[Byte]]]), registerRequired = true)
val codec = testCodecs.codec[TestClass]
val encoded = codec.encode(testClass).map(new String(_)).get
val codec = testCodecs.pureCodec[TestClass]
val encoded = codec.direct.rmap(new String(_)).unsafe(testClass)
val reasonableMaxSize = 20 // bytes
encoded should not contain "TestClass"
encoded.length should be < reasonableMaxSize

View File

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

View File

@ -1,21 +1,21 @@
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.4.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "4.1.0")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.18")
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.20")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.3.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.3.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.27")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.10.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.14.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.0")
addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.1")
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.1"
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.8.4"
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")