diff --git a/.build-scripts/build-project-simple.sh b/.build-scripts/build-project-simple.sh index ae1ccf6..bf8ab07 100755 --- a/.build-scripts/build-project-simple.sh +++ b/.build-scripts/build-project-simple.sh @@ -12,10 +12,10 @@ else echo "Build projects" echo "--------------------------------------------" echo "" - if [[ "$BRANCH_NAME" == "rc" ]] + if [[ "$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "release" ]] then sbt -d -J-Xmx2048m "; ++ ${scala_version}!; clean; test" - sbt -d -J-Xmx2048m "; ++ ${scala_version}!; packageBin; packageSrc; packageDoc" + sbt -d -J-Xmx2048m "; ++ ${scala_version}!; packagedArtifacts" else sbt -d -J-Xmx2048m "; ++ ${scala_version}!; clean; test; package" fi diff --git a/.build-scripts/build-project.sh b/.build-scripts/build-project.sh index 037efd5..994d95e 100755 --- a/.build-scripts/build-project.sh +++ b/.build-scripts/build-project.sh @@ -12,10 +12,10 @@ else echo "Build projects" echo "--------------------------------------------" echo "" - if [[ "$BRANCH_NAME" == "rc" ]] + if [[ "$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "release" ]] then sbt -d -J-Xmx2048m "; ++ ${scala_version}!; clean; coverage; test; coverageReport; coverageAggregate" - sbt -d -J-Xmx2048m "; ++ ${scala_version}!; packageBin; packageSrc; packageDoc" + sbt -d -J-Xmx2048m "; ++ ${scala_version}!; packagedArtifacts" else sbt -d -J-Xmx2048m "; ++ ${scala_version}!; clean; coverage; test; coverageReport; coverageAggregate; package" fi diff --git a/README.md b/README.md index 5488805..47fc166 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,15 @@ [![Build Status](https://semaphoreci.com/api/v1/kevin-lee/just-fp/branches/master/badge.svg)](https://semaphoreci.com/kevin-lee/just-fp) [![Download](https://api.bintray.com/packages/kevinlee/maven/just-fp/images/download.svg)](https://bintray.com/kevinlee/maven/just-fp/_latestVersion) +A small Functional Programming library. This is not meant to be an alternative to Scalaz or Cats. The reason for having this library is that in your project you don't want to have Scalaz or Cats as its dependency and you only need much smaller set of functional programming features than what Scalaz or Cats offers. So the users of your library can choose Scalaz or Cats to be used with your library. + # Getting Started In `build.sbt`, ```sbt -resolvers += "3rd Party Repo" at "https://dl.bintray.com/kevinlee/maven" +resolvers += "Just Repo" at "https://dl.bintray.com/kevinlee/maven" -libraryDependencies += "kevinlee" %% "just-fp" % "1.2.0" +libraryDependencies += "kevinlee" %% "just-fp" % "1.3.0" ``` then import @@ -17,3 +19,187 @@ then import import just.fp._ import just.fp.syntax._ ``` +or +```scala +import just.fp._, syntax._ +``` +In all the example code using `just-fp` below, I assume that you've already imported `just-fp` at the top. + +# Either + +## Right-Biased `Either` +`Either` in Scala prior to 2.12 is not right-biased meaning that you have to call `Either.right` all the time if you want to use it with `for-comprehension`. + +e.g.) Before 2.12 +```scala +for { + b <- methodReturningEither(a).right + c <- anotherReturningEither(b).right +} yield c +``` +If you use `just-fp`, it becomes +```scala +for { + b <- methodReturningEither(a) + c <- anotherReturningEither(b) +} yield c +``` +Of course, you don't need to do it if you use Scala 2.12 or higher. + +## Either Constructors +In normal ways, if you want to create `Left` or `Right`, you just use the `apply` methods of their companion objects (i.e. `Left()` `Right()`) A problem with this is that what these return is not `Either` but its data, `Left` or `Right`. + +You also need to specify not only type parameter for `Left` but also the one for `Right` when creating `Right`. + +e.g.) Without type parameters, +```scala +Right(1) +// Right[Nothing, Int] = Right(1) +``` +You don't want to have `Nothing` there. So do it with type parameters, +```scala +Right[String, Int](1) +// Right[String, Int] = Right(1) +``` +So it becomes unnecessarily verbose. Right should be inferred as the compiler knows it already yet to specify the left one, you have to put both left and right parameters. + +`Left`, of course, has the same problem. + +```scala +Left("error") +// Left[String, Nothing] = Left("error") +``` +```scala +Left[String, Int]("error") +// Left[String, Int] = Left("error") +``` + +Now with `just-fp`, it's simpler. You can use use `left` and `right` constructors as extension methods to the actual data values with only missing type info specified. + +e.g.) +```scala +1.right[String] // Now you only need to specify + // the missing type info only + // that is Left type parameter. +// Either[String, Int] = Right(1) +``` +For `Left`, +```scala +"error".left[Int] +// Either[String, Int] = Left("error") +``` + +## `leftMap` and `leftFlatMap` +So if you Scala 2.12 or higher or `just-fp` with the older Scala, `Either` is right-biassed. Then what about the `Left` case? Can I ever use `Left` for something useful like transforming the `Left` value to something else? + +For that, `just-fp` has added `leftMap` and `leftFlatMap` to `Either`. +e.g.) +```scala +for { + b <- f1(a).leftMap(AppError.calculatioError) + c <- f2(b).leftMap(AppError.fromComputeError) +} yield c +// f1 returns Either[String, Int] +// f2 returns Either[ComputeError, Int] +// The result is Either[AppError, Int] +``` + +# Option +## Option Constructors +Similar to `Either`, creating `Option` can expose its data instead of type. + +e.g.) The following code returns `Some[Int]` not `Option[Int]`. +```scala +Some(1) +// Some[Int] = Some(1) +``` + +```scala +None +// None.type = None +// Also None is None not Option[A] +``` + +With `just-fp`, +```scala +1.some +// Option[Int] = Some(1) + +none[String] +// Option[String] = None +``` + +# Type-safe Equal +`==` in Scala is not type safe so the following code can never be `true` as their types are different but it has no compile-time error. + +```scala +1 == "1" // always false, no compile-time error +``` + +`just-fp` has `Equal` typeclass with typeclass instances for value types (`Byte`, `Short`, `Int`, `Char`, `Long`, `Float` and `Double`) as well as `String`, `BigInt` and `BigDecimal`. + +It also has `Equal` typeclass instances for `WriterT`, `Writer` and `EitherT`. + +With `just-fp`, +```scala +// use triple equals from just-fp +1 === "1" // compile-time error +1 === 1 // true +"a" === "a" // true +1 !== 1 // false +1 !== 2 // true +``` + +If it's a `case class` the `equals` of which can be used for equality check, `NatualEqual` can be used. + +e.g.) +```scala +case class Foo(n: Int) +``` +```scala +Foo(1) === Foo(1) + ^ +error: value === is not a member of Foo +``` +This can be solved by `NatualEqual`. + +```scala +case class Foo(n: Int) + +object Foo { + implicit val eqaul: Equal[Foo] = Equal.equalA[Foo] +} + +Foo(1) === Foo(1) +// Boolean = true + +Foo(1) === Foo(2) +// Boolean = false + +Foo(1) !== Foo(1) +// Boolean = false + +Foo(1) !== Foo(2) +// Boolean = true +``` + +# Semi-Group +// To be updated ... + +# Monoid +// To be updated ... + +# Functor +// To be updated ... + +# Applicative +// To be updated ... + +# Monad +// To be updated ... + +# WriterT / Writer +// To be updated ... + +# EitherT +// To be updated ... diff --git a/build.sbt b/build.sbt index 4f8593c..a74a061 100644 --- a/build.sbt +++ b/build.sbt @@ -23,27 +23,32 @@ lazy val justFp = (project in file(".")) else Seq(sharedSourceDir / "scala-2.10_2.11") } - , resolvers += Resolver.sonatypeRepo("releases") - , addCompilerPlugin("org.typelevel" % "kind-projector" % "0.10.3" cross CrossVersion.binary) - , wartremoverErrors in (Compile, compile) ++= commonWarts - , wartremoverErrors in (Test, compile) ++= commonWarts - , resolvers += Deps.hedgehogRepo - , libraryDependencies ++= Deps.hedgehogLibs - , dependencyOverrides ++= crossVersionProps(Seq.empty[ModuleID], SemanticVersion.parseUnsafe(scalaVersion.value)) { - case (Major(2), Minor(10)) => - Seq("org.wartremover" %% "wartremover" % "2.3.7") - case x => - Seq.empty - } + , resolvers ++= Seq( + Resolver.sonatypeRepo("releases") + , Deps.hedgehogRepo + ) + , addCompilerPlugin("org.typelevel" % "kind-projector" % "0.11.0" cross CrossVersion.full) + , libraryDependencies := Deps.hedgehogLibs ++ + crossVersionProps(Seq.empty[ModuleID], SemanticVersion.parseUnsafe(scalaVersion.value)) { + case (Major(2), Minor(10)) => + libraryDependencies.value.filterNot(m => m.organization == "org.wartremover" && m.name == "wartremover") + case x => + libraryDependencies.value + } + , wartremoverErrors in (Compile, compile) ++= commonWarts((scalaBinaryVersion in update).value) + , wartremoverErrors in (Test, compile) ++= commonWarts((scalaBinaryVersion in update).value) , testFrameworks ++= Seq(TestFramework("hedgehog.sbt.Framework")) + , gitTagFrom := "release" + /* Bintray { */ , bintrayPackageLabels := Seq("Scala", "Functional Programming", "FP") , bintrayVcsUrl := Some("""git@github.com:Kevin-Lee/just-fp.git""") , licenses += ("MIT", url("http://opensource.org/licenses/MIT")) /* } Bintray */ - , initialCommands in console := """import just.fp._""" + , initialCommands in console := + """import just.fp._; import just.fp.syntax._""" /* Coveralls { */ , coverageHighlighting := (CrossVersion.partialVersion(scalaVersion.value) match { diff --git a/changelogs/1.3.0.md b/changelogs/1.3.0.md new file mode 100644 index 0000000..8848804 --- /dev/null +++ b/changelogs/1.3.0.md @@ -0,0 +1,62 @@ +## [1.3.0](https://github.com/Kevin-Lee/just-fp/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aclosed+milestone%3A%22milestone9%22) - 2019-10-13 + +### Done +* Add writer syntax (#89) + + e.g.) + ```scala + 1.writer("Get value") + // Writer[String,Int] = WriterT(("Get value",1)) + // WriterT[Id, String, Int] + + "something".writer(List("something happened")) + // Writer[List[String],String] = WriterT((List("something happened"),"something")) + // WriterT[Id, List[String], String] + ``` + +* Add `Writer` constructor for `(W, A)` (#87) + + e.g.) + ```scala + for { + a <- Writer.writer(("abc", 1)) + b <- Writer.writer(("def", a)) + } yield b + // WriterT[Id,String,Int] = WriterT(("abcdef",1)) + ``` + +* Add `mappend` syntax for `SemiGroup` and `Monoid` (#80) + + mappend syntax for SemiGroup and Monoid. append is too common name so there can be conflict in names with other types. So use mappend (short name for Monoid append) instead of append. + ```scala + 1.mappend(2) + // Int = 3 + + "abc".mappend("def") + // String = abcdef + + List(1, 2, 3).mappend(List(4, 5, 6)) + // List[Int] = List(1, 2, 3, 4, 5, 6) + ``` + +* Add `OptionSemiGroup` and `OptionMonoid` (#82) + + Add `OptionSemiGroup` to make `Option[A]` `SemiGroup[A]` and `OptionMonoid` to make `Option[A]` `Monoid[A]` if `SemiGroup[A]` exists. + ```scala + 1.some |+| 999.some // Option[Int] = Some(1000) + + 1.some.mappend(999.some) // Option[Int] = Some(1000) + + 123.some |+| none[Int] // Option[Int] = Some(123) + + none[Int] |+| 999.some // Option[Int] = Some(999) + + List(1, 2, 3).some |+| List(4, 5, 6).some + // Option[List[Int]] = Some(List(1, 2, 3, 4, 5, 6)) + + List(1, 2, 3).some |+| none[List[Int]] + // Option[List[Int]] = Some(List(1, 2, 3)) + + none[List[Int]] |+| List(1, 2, 3).some + // Option[List[Int]] = Some(List(1, 2, 3)) + ``` diff --git a/project/ProjectInfo.scala b/project/ProjectInfo.scala index 2f8536a..90ec79b 100644 --- a/project/ProjectInfo.scala +++ b/project/ProjectInfo.scala @@ -1,3 +1,5 @@ +import sbt.Keys.scalaBinaryVersion +import sbt.Keys.update import wartremover.WartRemover.autoImport.{Wart, Warts} /** @@ -9,9 +11,13 @@ object ProjectInfo { val ProjectScalaVersion: String = "2.13.1" val CrossScalaVersions: Seq[String] = Seq("2.10.7", "2.11.12", "2.12.10", ProjectScalaVersion) - val ProjectVersion: String = "1.2.0" + val ProjectVersion: String = "1.3.0" - val commonWarts: Seq[wartremover.Wart] = + def commonWarts(scalaBinaryVersion: String): Seq[wartremover.Wart] = scalaBinaryVersion match { + case "2.10" => + Seq.empty + case _ => Warts.allBut(Wart.DefaultArguments, Wart.Overloading, Wart.Any, Wart.Nothing, Wart.NonUnitStatements) + } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 7cff254..804dac2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ logLevel := sbt.Level.Warn addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.5") -addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.2") +addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.3") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") diff --git a/src/main/scala/just/fp/EitherT.scala b/src/main/scala/just/fp/EitherT.scala index 80a224d..6482140 100644 --- a/src/main/scala/just/fp/EitherT.scala +++ b/src/main/scala/just/fp/EitherT.scala @@ -93,7 +93,7 @@ sealed abstract class EitherTFunctorInstance { } sealed abstract class EitherTApplicativeInstance extends EitherTFunctorInstance { - implicit def eitherTFunctor[F[_], A](implicit F0: Applicative[F]): Applicative[EitherT[F, A, *]] = + implicit def eitherTApplicative[F[_], A](implicit F0: Applicative[F]): Applicative[EitherT[F, A, *]] = new EitherTApplicative[F, A] { override implicit val F: Applicative[F] = F0 } diff --git a/src/main/scala/just/fp/Monoid.scala b/src/main/scala/just/fp/Monoid.scala index c20b2cf..aca2ad5 100644 --- a/src/main/scala/just/fp/Monoid.scala +++ b/src/main/scala/just/fp/Monoid.scala @@ -33,7 +33,7 @@ trait Monoid[A] extends SemiGroup[A] { def monoidLaw: MonoidLaw = new MonoidLaw {} } -object Monoid { +object Monoid extends OptionMonoidInstance { implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] with ListSemiGroup[A] { override def zero: List[A] = Nil @@ -75,4 +75,14 @@ object Monoid { override def zero: BigDecimal = BigDecimal(0) } -} \ No newline at end of file +} + +private[fp] trait OptionMonoidInstance extends OptionSemiGroupInstance { + implicit def optionMonoid[A](implicit F0: SemiGroup[A]): Monoid[Option[A]] = + new Monoid[Option[A]] with OptionSemigroup[A] { + + override implicit def F: SemiGroup[A] = F0 + + override def zero: Option[A] = None + } +} diff --git a/src/main/scala/just/fp/SemiGroup.scala b/src/main/scala/just/fp/SemiGroup.scala index 4e194c7..bf2d842 100644 --- a/src/main/scala/just/fp/SemiGroup.scala +++ b/src/main/scala/just/fp/SemiGroup.scala @@ -8,7 +8,7 @@ trait SemiGroup[A] { def append(a1: A, a2: => A): A } -object SemiGroup { +object SemiGroup extends OptionSemiGroupInstance { trait ListSemiGroup[A] extends SemiGroup[List[A]] { override def append(a1: List[A], a2: => List[A]): List[A] = a1 ++ a2 @@ -60,3 +60,27 @@ object SemiGroup { } implicit val bigDecimalSemiGroup: SemiGroup[BigDecimal] = new BigDecimalSemiGroup {} } + +private[fp] trait OptionSemigroup[A] extends SemiGroup[Option[A]] { + + implicit def F: SemiGroup[A] + + override def append(a1: Option[A], a2: => Option[A]): Option[A] = (a1, a2) match { + case (Some(x), Some(y)) => + Some(implicitly[SemiGroup[A]].append(x, y)) + case (Some(x), None) => + Some(x) + case (None, Some(y)) => + Some(y) + case (None, None) => + None + } +} + +private[fp] trait OptionSemiGroupInstance { + + implicit def optionSemigroup[A](implicit F0: SemiGroup[A]): SemiGroup[Option[A]] = + new OptionSemigroup[A] { + override implicit def F: SemiGroup[A] = F0 + } +} \ No newline at end of file diff --git a/src/main/scala/just/fp/WriterT.scala b/src/main/scala/just/fp/WriterT.scala index e5856c6..a01ed96 100644 --- a/src/main/scala/just/fp/WriterT.scala +++ b/src/main/scala/just/fp/WriterT.scala @@ -65,14 +65,14 @@ private trait WriterTMonad[F[_], W] extends Monad[WriterT[F, W, *]] with WriterT sealed abstract class WriterTFunctorInstance { - implicit def writerTMonad[F[_], W](implicit F0: Functor[F]): Functor[WriterT[F, W, *]] = + implicit def writerTFunctor[F[_], W](implicit F0: Functor[F]): Functor[WriterT[F, W, *]] = new WriterTFunctor[F, W] { implicit val F: Functor[F] = F0 } } sealed abstract class WriterTApplicativeInstance extends WriterTFunctorInstance { - implicit def writerTMonad[F[_], W](implicit F0: Applicative[F], S0: Monoid[W]): Applicative[WriterT[F, W, *]] = + implicit def writerTApplicative[F[_], W](implicit F0: Applicative[F], S0: Monoid[W]): Applicative[WriterT[F, W, *]] = new WriterTApplicative[F, W] { override implicit val F: Applicative[F] = F0 implicit val W: Monoid[W] = S0 diff --git a/src/main/scala/just/fp/package.scala b/src/main/scala/just/fp/package.scala index 80ca6ea..8d26804 100644 --- a/src/main/scala/just/fp/package.scala +++ b/src/main/scala/just/fp/package.scala @@ -11,6 +11,8 @@ package object fp extends IdInstance { type Writer[L, V] = WriterT[Id, L, V] object Writer { - def apply[W, A](w: W, a: A): WriterT[Id, W, A] = WriterT[Id, W, A]((w, a)) + def apply[W, A](w: W, a: A): Writer[W, A] = Writer.writer((w, a)) + + def writer[W, A](wa: (W, A)): Writer[W, A] = WriterT[Id, W, A](wa) } } diff --git a/src/main/scala/just/fp/syntax/SemiGroupSyntax.scala b/src/main/scala/just/fp/syntax/SemiGroupSyntax.scala index 51c75e8..8eff87a 100644 --- a/src/main/scala/just/fp/syntax/SemiGroupSyntax.scala +++ b/src/main/scala/just/fp/syntax/SemiGroupSyntax.scala @@ -11,6 +11,8 @@ import scala.language.implicitConversions object SemiGroupSyntax { final class SemiGroupOps[A: SemiGroup] private[syntax](val a1: A) { def |+|(a2: A): A = implicitly[SemiGroup[A]].append(a1, a2) + + def mappend(a2: A): A = implicitly[SemiGroup[A]].append(a1, a2) } } diff --git a/src/main/scala/just/fp/syntax/WriterSyntax.scala b/src/main/scala/just/fp/syntax/WriterSyntax.scala new file mode 100644 index 0000000..1cf1f78 --- /dev/null +++ b/src/main/scala/just/fp/syntax/WriterSyntax.scala @@ -0,0 +1,22 @@ +package just.fp.syntax + +import just.fp.Writer + +import scala.language.implicitConversions + +/** + * @author Kevin Lee + * @since 2019-10-02 + */ +object WriterOps { + final class ToWriter[A](val a: A) extends AnyVal { + def writer[W](w: W): Writer[W, A] = Writer(w, a) + } +} + +trait WriterSyntax { + import WriterOps._ + + @SuppressWarnings(Array("org.wartremover.warts.ImplicitConversion")) + implicit final def toWriter[A](a: A): ToWriter[A] = new ToWriter(a) +} diff --git a/src/main/scala/just/fp/syntax/package.scala b/src/main/scala/just/fp/syntax/package.scala index ba3d760..c2bb398 100644 --- a/src/main/scala/just/fp/syntax/package.scala +++ b/src/main/scala/just/fp/syntax/package.scala @@ -9,3 +9,4 @@ package object syntax with OptionSyntax with EitherSyntax with SemiGroupSyntax + with WriterSyntax diff --git a/src/test/scala/just/fp/MonoidSpec.scala b/src/test/scala/just/fp/MonoidSpec.scala index fb66509..f9c59ba 100644 --- a/src/test/scala/just/fp/MonoidSpec.scala +++ b/src/test/scala/just/fp/MonoidSpec.scala @@ -10,7 +10,8 @@ import hedgehog.runner._ object MonoidSpec extends Properties { override def tests: List[Test] = List( - property("testListMonoidLaw", ListMonoidLaws.laws) + property("testOptionMonoidLaw", OptionMonoidLaws.laws) + , property("testListMonoidLaw", ListMonoidLaws.laws) , property("testVectorMonoidLaw", VectorMonoidLaws.laws) , property("testStringMonoidLaw", StringMonoidLaws.laws) , property("testByteMonoidLaw", ByteMonoidLaws.laws) @@ -40,6 +41,15 @@ object MonoidSpec extends Properties { ) } + object OptionMonoidLaws { + def genOption: Gen[Option[Int]] = Gens.genOption(Gens.genIntFromMinToMax) + + def laws: Property = + Specs.monoidLaws.laws[Option[Int]]( + genOption + ) + } + object StringMonoidLaws { def genString: Gen[String] = Gens.genUnicodeString diff --git a/src/test/scala/just/fp/WriterTSpec.scala b/src/test/scala/just/fp/WriterTSpec.scala index e343c79..ea6e849 100644 --- a/src/test/scala/just/fp/WriterTSpec.scala +++ b/src/test/scala/just/fp/WriterTSpec.scala @@ -16,6 +16,7 @@ object WriterTSpec extends Properties { , property("testWriterFunctorLaws", WriterFunctorLaws.laws) , property("testWriterApplicativeLaws", WriterApplicativeLaws.laws) , property("testWriterMonadLaws", WriterMonadLaws.laws) + , property("test Writer(w, a) should be equal to Writer.writer((w, a))", testWriterApplyAndWriter) ) object WriterTFunctorLaws { @@ -98,4 +99,16 @@ object WriterTSpec extends Properties { , Gens.genAToMonadA(Gens.genIntToInt) ) } + + def testWriterApplyAndWriter: Property = for { + w <- Gens.genUnicodeString.log("w") + a <- Gens.genIntFromMinToMax.log("a") + wa = (w, a) + } yield { + val writer1 = Writer.writer(wa) + val writer2 = Writer(w, a) + + import just.fp.syntax._ + Result.diffNamed("=== Not Equal ===", writer1, writer2)(_ === _) + } } diff --git a/src/test/scala/just/fp/syntax/SemiGroupSyntaxSpec.scala b/src/test/scala/just/fp/syntax/SemiGroupSyntaxSpec.scala index 1379f4c..3ddb8cc 100644 --- a/src/test/scala/just/fp/syntax/SemiGroupSyntaxSpec.scala +++ b/src/test/scala/just/fp/syntax/SemiGroupSyntaxSpec.scala @@ -12,8 +12,9 @@ object SemiGroupSyntaxSpec extends Properties { import just.fp._ override def tests: List[Test] = List( - property("List |+| List", testPlus(Gens.genList(Gens.genIntFromMinToMax, 10))) - , property("Vector |+| Vector", testPlus(Gens.genVector(Gens.genIntFromMinToMax, 10))) + property("test Option |+| Option", testPlus(Gens.genOption(Gens.genIntFromMinToMax))) + , property("test List |+| List", testPlus(Gens.genList(Gens.genIntFromMinToMax, 10))) + , property("test Vector |+| Vector", testPlus(Gens.genVector(Gens.genIntFromMinToMax, 10))) , property("test String |+| String", testPlus(Gens.genUnicodeString)) , property("test Byte |+| Byte", testPlus(Gens.genByteFromMinToMax)) , property("test Short |+| Short", testPlus(Gens.genShortFromMinToMax)) @@ -22,6 +23,17 @@ object SemiGroupSyntaxSpec extends Properties { , property("test Long |+| Long", testPlus(Gens.genLongFromMinToMax)) , property("test BigInt |+| BigInt", testPlus(Gens.genBigInt)) , property("test BigDecimal |+| BigDecimal", testPlus(Gens.genBigDecimal)) + , property("test Option.mappend(Option)", testMappend(Gens.genOption(Gens.genIntFromMinToMax))) + , property("test List.mappend(List)", testMappend(Gens.genList(Gens.genIntFromMinToMax, 10))) + , property("test Vector.mappend(Vector)", testMappend(Gens.genVector(Gens.genIntFromMinToMax, 10))) + , property("test String.mappend(String)", testMappend(Gens.genUnicodeString)) + , property("test Byte.mappend(Byte)", testMappend(Gens.genByteFromMinToMax)) + , property("test Short.mappend(Short)", testMappend(Gens.genShortFromMinToMax)) + , property("test Char.mappend(Char)", testMappend(Gens.genCharFromMinToMax)) + , property("test Int.mappend(Int)", testMappend(Gens.genIntFromMinToMax)) + , property("test Long.mappend(Long)", testMappend(Gens.genLongFromMinToMax)) + , property("test BigInt.mappend(BigInt)", testMappend(Gens.genBigInt)) + , property("test BigDecimal.mappend(BigDecimal)", testMappend(Gens.genBigDecimal)) ) def testPlus[A: SemiGroup](genA: Gen[A]): Property = for { @@ -29,4 +41,9 @@ object SemiGroupSyntaxSpec extends Properties { a2 <- genA.log("a2") } yield (a1 |+| a2) ==== implicitly[SemiGroup[A]].append(a1, a2) + def testMappend[A: SemiGroup](genA: Gen[A]): Property = for { + a1 <- genA.log("a1") + a2 <- genA.log("a2") + } yield (a1.mappend(a2)) ==== implicitly[SemiGroup[A]].append(a1, a2) + } diff --git a/src/test/scala/just/fp/syntax/WriterSyntaxSpec.scala b/src/test/scala/just/fp/syntax/WriterSyntaxSpec.scala new file mode 100644 index 0000000..127e435 --- /dev/null +++ b/src/test/scala/just/fp/syntax/WriterSyntaxSpec.scala @@ -0,0 +1,28 @@ +package just.fp.syntax + +import hedgehog._ +import hedgehog.runner._ + +import just.fp.Gens + +/** + * @author Kevin Lee + * @since 2019-10-02 + */ +object WriterSyntaxSpec extends Properties { + override def tests: List[Test] = List( + property("test value.writer(w) syntax", testValueWriter) + ) + + def testValueWriter: Property = for { + a <- Gens.genIntFromMinToMax.log("a") + w <- Gens.genUnicodeString.log("w") + } yield { + import just.fp._ + + val expected = Writer(w, a) + val actual = a.writer(w) + + actual ==== expected + } +}