From 1aebabd2f6f228cf475e60f73ecdf87766ca29f6 Mon Sep 17 00:00:00 2001 From: Nicolas Rinaudo Date: Sun, 29 Dec 2019 18:00:10 +0100 Subject: [PATCH] Simplify laws --- .../kantan/codecs/laws/DecoderLaws.scala | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/DecoderLaws.scala b/laws/shared/src/main/scala/kantan/codecs/laws/DecoderLaws.scala index b53cbdec..4824f1f3 100644 --- a/laws/shared/src/main/scala/kantan/codecs/laws/DecoderLaws.scala +++ b/laws/shared/src/main/scala/kantan/codecs/laws/DecoderLaws.scala @@ -18,52 +18,65 @@ package kantan.codecs.laws import kantan.codecs.Decoder import kantan.codecs.laws.CodecValue.{IllegalValue, LegalValue} -import org.scalacheck.Prop +/** Laws that a [[Decoder]] must abide by to be considered lawful. + * + * These are executed through [[kantan.codecs.discipline.DecoderTests]]. + */ trait DecoderLaws[Encoded, Decoded, Failure, Tag] { - def decoder: Decoder[Encoded, Decoded, Failure, Tag] - private def cmp(result: Either[Failure, Decoded], cv: CodecValue[Encoded, Decoded, Tag]): Boolean = - (cv, result) match { - case (IllegalValue(_), Left(_)) => true - case (LegalValue(_, d), Right(d2)) => d == d2 - case _ => false - } + /** Decoder whose lawfulness is being checked. */ + def decoder: Decoder[Encoded, Decoded, Failure, Tag] // - Simple laws ----------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------------- - def decode(v: CodecValue[Encoded, Decoded, Tag]): Boolean = cmp(decoder.decode(v.encoded), v) + // These two laws are the foundation on which the others are built: if we can trust that `decode` is well behaved, + // then relying on it in all other laws is sane. + /** Decoding a legal value must yield the expected result. */ + def decode(v: LegalValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == Right(v.decoded) + + /** Decoding an illegal value must fail. */ def decodeFailure(v: IllegalValue[Encoded, Decoded, Tag]): Boolean = - Prop.throws(classOf[Exception])(decoder.unsafeDecode(v.encoded)) + decoder.decode(v.encoded).isLeft // - Functor laws ---------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------------- + /** Mapping on `identity` should not change the decoder's behaviour. */ def mapIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == decoder.map(identity).decode(v.encoded) + /** Mapping on the composition of two functions must be the same as mapping on the first, then the second. */ def mapComposition[A, B](v: CodecValue[Encoded, Decoded, Tag], f: Decoded => A, g: A => B): Boolean = decoder.map(f andThen g).decode(v.encoded) == decoder.map(f).map(g).decode(v.encoded) + // - Bifunctor laws -------------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------- + /** Same as [[mapIdentity]], but for `leftMap`. */ def leftMapIdentity[A](v: CodecValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == decoder.leftMap(identity).decode(v.encoded) + /** Same as [[mapComposition]], but for `leftMap`. */ def leftMapComposition[A, B](v: CodecValue[Encoded, Decoded, Tag], f: Failure => A, g: A => B): Boolean = decoder.leftMap(f andThen g).decode(v.encoded) == decoder.leftMap(f).leftMap(g).decode(v.encoded) // - Contravariant laws ---------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------------- + /** Same as [[mapIdentity]], but for `contramapEncoded`. */ def contramapEncodedIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == decoder.contramapEncoded(identity[Encoded]).decode(v.encoded) + /** Same as [[mapComposition]], but for `contramapEncoded`. */ def contramapEncodedComposition[A, B](b: B, f: A => Encoded, g: B => A): Boolean = decoder.contramapEncoded(g andThen f).decode(b) == decoder.contramapEncoded(f).contramapEncoded(g).decode(b) // - "Kleisli" laws -------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------------- + /** Same as [[mapIdentity]], but for `emap`. */ def emapIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == decoder.emap(Right.apply).decode(v.encoded) + /** Same as [[mapComposition]], but for `emap`. */ def emapComposition[A, B]( v: CodecValue[Encoded, Decoded, Tag], f: Decoded => Either[Failure, A], @@ -76,6 +89,11 @@ trait DecoderLaws[Encoded, Decoded, Failure, Tag] { } object DecoderLaws { + + /** Creates an instance of [[DecoderLaws]] for any type that has a valid decoder instance. + * + * There rarely is a need to call this directly. Consider using [[kantan.codecs.discipline.DecoderTests]] instead. + */ def apply[E, D, F, T](implicit de: Decoder[E, D, F, T]): DecoderLaws[E, D, F, T] = new DecoderLaws[E, D, F, T] { override val decoder = de