diff --git a/build.sbt b/build.sbt index 60c4b770..57cb8c5f 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ ThisBuild / name := "scala-with-cats" ThisBuild / organization := "com.scalawithcats" ThisBuild / version := "0.0.1" -ThisBuild / scalaVersion := "2.13.8" +ThisBuild / scalaVersion := "3.2.2" ThisBuild / useSuperShell := false Global / logLevel := Level.Warn @@ -14,11 +14,14 @@ enablePlugins(MdocPlugin) mdocIn := sourceDirectory.value / "pages" mdocOut := target.value / "pages" -val catsVersion = "2.7.0" +scalacOptions ++= Seq( + "-explain", // Better diagnostics + "-Ykind-projector:underscores" // In-lieu of kind-projector +) -libraryDependencies ++= Seq("org.typelevel" %% "cats-core" % catsVersion) +val catsVersion = "2.9.0" -addCompilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full) +libraryDependencies ++= Seq("org.typelevel" %% "cats-core" % catsVersion) mdocVariables := Map( "SCALA_VERSION" -> scalaVersion.value, diff --git a/package.json b/package.json index 4287edb5..42517805 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "devDependencies": { "bootstrap": "^3.4.1", "coffeeify": "1.0.0", + "coffeescript": "^2.5.1", "jquery": "3.5.0", - "uglifyify": "2.6.0", "lessc": "^1.0.2", - "underscore": "1.7.0", "pandoc-filter": "0.1.6", - "coffeescript": "^2.5.1" + "uglifyify": "2.6.0", + "underscore": "1.7.0" }, "author": "Noel Welsh and Dave Gurnell" } diff --git a/project/Pandoc.scala b/project/Pandoc.scala index c5ad2229..1c940481 100644 --- a/project/Pandoc.scala +++ b/project/Pandoc.scala @@ -118,7 +118,7 @@ object Pandoc { "--table-of-contents", "--highlight-style tango", "--standalone", - "--self-contained", + "--embed-resources", ), extras, metadata, diff --git a/project/build.properties b/project/build.properties index 3161d214..46e43a97 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.8.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index 7e3fed96..b1563996 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.0" ) +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7" ) // addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.10") diff --git a/src/pages/adt/scala.md b/src/pages/adt/scala.md index 95b1593c..93e93998 100644 --- a/src/pages/adt/scala.md +++ b/src/pages/adt/scala.md @@ -20,10 +20,8 @@ Not everyone makes their case classes `final`, but they should. A non-`final` ca A logical or (a sum type) is represented by an `enum`. For the sum type `A` is a `B` **or** `C` the Scala 3 representation is ```scala -enum A { - case B - case C -} +enum A: + case B, C ``` There are a few wrinkles to be aware of. @@ -37,13 +35,12 @@ If we have a sum of products, such as: the representation is ```scala -enum A { +enum A: case B(d: D, e: E) case C(f: F, g: G) -} ``` -In other words you can't write `final case class` inside an `enum`. You also can't nest `enum` inside `enum`. Nexted logical ors can be rewritten into a single logical or containing only logical ands (known as disjunctive normal form) so this is not a limitation in practice. However the Scala 2 representation is still available in Scala 3 should you want more expressivity. +In other words you can't write `final case class` inside an `enum`. You also can't nest `enum` inside `enum`. Nested logical ors can be rewritten into a single logical or containing only logical ands (known as disjunctive normal form) so this is not a limitation in practice. However the Scala 2 representation is still available in Scala 3 should you want more expressivity. ### Algebraic Data Types in Scala 2 diff --git a/src/pages/applicatives/applicative.md b/src/pages/applicatives/applicative.md index 3976df38..db0edaf3 100644 --- a/src/pages/applicatives/applicative.md +++ b/src/pages/applicatives/applicative.md @@ -24,16 +24,14 @@ introduced in Chapter [@sec:monads]. Here's a simplified definition in code: ```scala -trait Apply[F[_]] extends Semigroupal[F] with Functor[F] { +trait Apply[F[_]] extends Semigroupal[F] with Functor[F]: def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = ap(map(fa)(a => (b: B) => (a, b)))(fb) -} -trait Applicative[F[_]] extends Apply[F] { +trait Applicative[F[_]] extends Apply[F]: def pure[A](a: A): F[A] -} ``` Breaking this down, the `ap` method applies a parameter `fa` diff --git a/src/pages/applicatives/examples.md b/src/pages/applicatives/examples.md index 60c22450..b85bc63f 100644 --- a/src/pages/applicatives/examples.md +++ b/src/pages/applicatives/examples.md @@ -12,9 +12,9 @@ provide parallel as opposed to sequential execution: ```scala mdoc:silent import cats.Semigroupal -import cats.instances.future._ // for Semigroupal -import scala.concurrent._ -import scala.concurrent.duration._ +import cats.instances.future.* // for Semigroupal +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global val futurePair = Semigroupal[Future]. @@ -31,7 +31,7 @@ by the time we call `product`. We can use apply syntax to zip fixed numbers of `Futures`: ```scala mdoc:silent -import cats.syntax.apply._ // for mapN +import cats.syntax.apply.* // for mapN case class Cat( name: String, @@ -59,7 +59,7 @@ but we actually get the cartesian product of their elements: ```scala mdoc:silent import cats.Semigroupal -import cats.instances.list._ // for Semigroupal +import cats.instances.list.* // for Semigroupal ``` ```scala mdoc @@ -81,7 +81,7 @@ we find that `product` implements the same fail-fast behaviour as `flatMap`: ```scala mdoc:silent -import cats.instances.either._ // for Semigroupal +import cats.instances.either.* // for Semigroupal type ErrorOr[A] = Either[Vector[String], A] ``` @@ -105,8 +105,8 @@ If we have a monad we can implement `product` as follows. ```scala mdoc:silent import cats.Monad -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatmap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatmap def product[F[_]: Monad, A, B](fa: F[A], fb: F[B]): F[(A,B)] = fa.flatMap(a => @@ -178,8 +178,8 @@ the definition of `product` in terms of import cats.Monad ``` ```scala mdoc:silent -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatMap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatMap def product[F[_]: Monad, A, B](x: F[A], y: F[B]): F[(A, B)] = x.flatMap(a => y.map(b => (a, b))) @@ -189,8 +189,8 @@ This code is equivalent to a for comprehension: ```scala mdoc:invisible:reset-object import cats.Monad -import cats.syntax.flatMap._ // for flatMap -import cats.syntax.functor._ // for map +import cats.syntax.flatMap.* // for flatMap +import cats.syntax.functor.* // for map ``` ```scala mdoc:silent def product[F[_]: Monad, A, B](x: F[A], y: F[B]): F[(A, B)] = @@ -204,7 +204,7 @@ The semantics of `flatMap` are what give rise to the behaviour for `List` and `Either`: ```scala mdoc:silent -import cats.instances.list._ // for Semigroupal +import cats.instances.list.* // for Semigroupal ``` ```scala mdoc diff --git a/src/pages/applicatives/index.md b/src/pages/applicatives/index.md index cb45b25a..70211cee 100644 --- a/src/pages/applicatives/index.md +++ b/src/pages/applicatives/index.md @@ -19,7 +19,7 @@ fails on the first call to `parseInt` and doesn't go any further: ```scala mdoc:silent -import cats.syntax.either._ // for catchOnly +import cats.syntax.either.* // for catchOnly def parseInt(str: String): Either[String, Int] = Either.catchOnly[NumberFormatException](str.toInt). diff --git a/src/pages/applicatives/parallel.md b/src/pages/applicatives/parallel.md index 8089891f..3f9191a0 100644 --- a/src/pages/applicatives/parallel.md +++ b/src/pages/applicatives/parallel.md @@ -17,7 +17,7 @@ stops at the first error. ```scala mdoc:silent import cats.Semigroupal -import cats.instances.either._ // for Semigroupal +import cats.instances.either.* // for Semigroupal type ErrorOr[A] = Either[Vector[String], A] val error1: ErrorOr[Int] = Left(Vector("Error 1")) @@ -33,8 +33,8 @@ using `tupled` as a short-cut. ```scala mdoc:silent -import cats.syntax.apply._ // for tupled -import cats.instances.vector._ // for Semigroup on Vector +import cats.syntax.apply.* // for tupled +import cats.instances.vector.* // for Semigroup on Vector ``` ```scala mdoc (error1, error2).tupled @@ -45,7 +45,7 @@ we simply replace `tupled` with its "parallel" version called `parTupled`. ```scala mdoc:silent -import cats.syntax.parallel._ // for parTupled +import cats.syntax.parallel.* // for parTupled ``` ```scala mdoc (error1, error2).parTupled @@ -57,7 +57,7 @@ Any type that has a `Semigroup` instance will work. For example, here we use `List` instead. ```scala mdoc:silent -import cats.instances.list._ // for Semigroup on List +import cats.instances.list.* // for Semigroup on List type ErrorOrList[A] = Either[List[String], A] val errStr1: ErrorOrList[Int] = Left(List("error 1")) @@ -88,13 +88,12 @@ Let's dig into how `Parallel` works. The definition below is the core of `Parallel`. ```scala -trait Parallel[M[_]] { +trait Parallel[M[_]]: type F[_] def applicative: Applicative[F] def monad: Monad[M] def parallel: ~>[M, F] -} ``` This tells us if there is a `Parallel` instance for some type constructor `M` then: @@ -115,13 +114,11 @@ by defining a `FunctionK` that converts an `Option` to a `List`. ```scala mdoc:silent import cats.arrow.FunctionK -object optionToList extends FunctionK[Option, List] { +object optionToList extends FunctionK[Option, List]: def apply[A](fa: Option[A]): List[A] = - fa match { + fa match case None => List.empty[A] case Some(a) => List(a) - } -} ``` ```scala mdoc optionToList(Some(1)) @@ -160,7 +157,7 @@ insted of creating the cartesian product. We can see by writing a little bit of code. ```scala mdoc:silent -import cats.instances.list._ +import cats.instances.list.* ``` ```scala mdoc (List(1, 2), List(3, 4)).tupled diff --git a/src/pages/applicatives/semigroupal.md b/src/pages/applicatives/semigroupal.md index 09bba7d1..c1aca06a 100644 --- a/src/pages/applicatives/semigroupal.md +++ b/src/pages/applicatives/semigroupal.md @@ -7,9 +7,8 @@ a `Semigroupal[F]` allows us to combine them to form an `F[(A, B)]`. Its definition in Cats is: ```scala -trait Semigroupal[F[_]] { +trait Semigroupal[F[_]]: def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] -} ``` As we discussed at the beginning of this chapter, @@ -34,7 +33,7 @@ Let's join some `Options` as an example: ```scala mdoc:silent:reset-object import cats.Semigroupal -import cats.instances.option._ // for Semigroupal +import cats.instances.option.* // for Semigroupal ``` ```scala mdoc @@ -59,7 +58,7 @@ For example, the methods `tuple2` through `tuple22` generalise `product` to different arities: ```scala mdoc:silent -import cats.instances.option._ // for Semigroupal +import cats.instances.option.* // for Semigroupal ``` ```scala mdoc @@ -98,8 +97,8 @@ We import the syntax from [`cats.syntax.apply`][cats.syntax.apply]. Here's an example: ```scala mdoc:silent -import cats.instances.option._ // for Semigroupal -import cats.syntax.apply._ // for tupled and mapN +import cats.instances.option.* // for Semigroupal +import cats.syntax.apply.* // for tupled and mapN ``` The `tupled` method is implicitly added to the tuple of `Options`. @@ -167,11 +166,11 @@ Here's an example: ```scala mdoc:silent:reset-object import cats.Monoid -import cats.instances.int._ // for Monoid -import cats.instances.invariant._ // for Semigroupal -import cats.instances.list._ // for Monoid -import cats.instances.string._ // for Monoid -import cats.syntax.apply._ // for imapN +import cats.instances.int.* // for Monoid +import cats.instances.invariant.* // for Semigroupal +import cats.instances.list.* // for Monoid +import cats.instances.string.* // for Monoid +import cats.syntax.apply.* // for imapN final case class Cat( name: String, @@ -185,7 +184,7 @@ val tupleToCat: (String, Int, List[String]) => Cat = val catToTuple: Cat => (String, Int, List[String]) = cat => (cat.name, cat.yearOfBirth, cat.favoriteFoods) -implicit val catMonoid: Monoid[Cat] = ( +given catMonoid: Monoid[Cat] = ( Monoid[String], Monoid[Int], Monoid[List[String]] @@ -196,7 +195,7 @@ Our `Monoid` allows us to create "empty" `Cats`, and add `Cats` together using the syntax from Chapter [@sec:monoids]: ```scala mdoc:silent -import cats.syntax.semigroup._ // for |+| +import cats.syntax.semigroup.* // for |+| val garfield = Cat("Garfield", 1978, List("Lasagne")) val heathcliff = Cat("Heathcliff", 1988, List("Junk Food")) diff --git a/src/pages/applicatives/validated.md b/src/pages/applicatives/validated.md index 97f917af..478902bd 100644 --- a/src/pages/applicatives/validated.md +++ b/src/pages/applicatives/validated.md @@ -21,7 +21,7 @@ is therefore free to accumulate errors: ```scala mdoc:silent import cats.Semigroupal import cats.data.Validated -import cats.instances.list._ // for Monoid +import cats.instances.list.* // for Monoid type AllErrorsOr[A] = Validated[List[String], A] ``` @@ -59,7 +59,7 @@ which widen the return type to `Validated`: ```scala mdoc:invisible:reset-object import cats.data.Validated -import cats.instances.list._ // for Monoid +import cats.instances.list.* // for Monoid type AllErrorsOr[A] = Validated[List[String], A] ``` @@ -73,7 +73,7 @@ the `valid` and `invalid` extension methods from `cats.syntax.validated`: ```scala mdoc:silent -import cats.syntax.validated._ // for valid and invalid +import cats.syntax.validated.* // for valid and invalid ``` ```scala mdoc @@ -87,8 +87,8 @@ and [`cats.syntax.applicativeError`][cats.syntax.applicativeError] respectively: ```scala mdoc:silent -import cats.syntax.applicative._ // for pure -import cats.syntax.applicativeError._ // for raiseError +import cats.syntax.applicative.* // for pure +import cats.syntax.applicativeError.* // for raiseError type ErrorsOr[A] = Validated[List[String], A] ``` @@ -130,7 +130,7 @@ number of parameters for `Semigroupal`: ```scala mdoc:invisible:reset-object import cats.data.Validated import cats.Semigroupal -import cats.syntax.validated._ +import cats.syntax.validated.* ``` ```scala mdoc:silent type AllErrorsOr[A] = Validated[String, A] @@ -149,7 +149,7 @@ Once we import a `Semigroup` for the error type, everything works as expected: ```scala mdoc:silent -import cats.instances.string._ // for Semigroup +import cats.instances.string.* // for Semigroup ``` ```scala mdoc @@ -163,7 +163,7 @@ or any of the other `Semigroupal` methods to accumulate errors as we like: ```scala mdoc:silent -import cats.syntax.apply._ // for tupled +import cats.syntax.apply.* // for tupled ``` ```scala mdoc @@ -178,7 +178,7 @@ for accumulating errors. We commonly use `Lists` or `Vectors` instead: ```scala mdoc:silent -import cats.instances.vector._ // for Semigroupal +import cats.instances.vector.* // for Semigroupal ``` ```scala mdoc @@ -246,7 +246,7 @@ using the `toEither` and `toValidated` methods. Note that `toValidated` comes from [`cats.syntax.either`]: ```scala mdoc -import cats.syntax.either._ // for toValidated +import cats.syntax.either.* // for toValidated "Badness".invalid[Int] "Badness".invalid[Int].toEither @@ -371,7 +371,7 @@ and we use `leftMap` to turn it into an error message: ```scala mdoc:silent -import cats.syntax.either._ // for catchOnly +import cats.syntax.either.* // for catchOnly type NumFmtExn = NumberFormatException @@ -468,8 +468,8 @@ We can do this by switching from `Either` to `Validated` and using apply syntax: ```scala mdoc:silent -import cats.instances.list._ // for Semigroupal -import cats.syntax.apply._ // for mapN +import cats.instances.list.* // for Semigroupal +import cats.syntax.apply.* // for mapN def readUser(data: FormData): FailSlow[User] = ( diff --git a/src/pages/case-studies/crdt/abstraction.md b/src/pages/case-studies/crdt/abstraction.md index 52d214db..64731507 100644 --- a/src/pages/case-studies/crdt/abstraction.md +++ b/src/pages/case-studies/crdt/abstraction.md @@ -27,48 +27,38 @@ represent the key and value types of the map abstraction. ```scala mdoc:reset-object:invisible import cats.kernel.CommutativeMonoid -trait BoundedSemiLattice[A] extends CommutativeMonoid[A] { +trait BoundedSemiLattice[A] extends CommutativeMonoid[A]: def combine(a1: A, a2: A): A def empty: A -} -object BoundedSemiLattice { - implicit val intInstance: BoundedSemiLattice[Int] = - new BoundedSemiLattice[Int] { - def combine(a1: Int, a2: Int): Int = - a1 max a2 +given intInstance: BoundedSemiLattice[Int] with + def combine(a1: Int, a2: Int): Int = + a1 max a2 - val empty: Int = - 0 - } + val empty: Int = 0 - implicit def setInstance[A]: BoundedSemiLattice[Set[A]] = - new BoundedSemiLattice[Set[A]]{ - def combine(a1: Set[A], a2: Set[A]): Set[A] = - a1 union a2 +given setInstance[A]: BoundedSemiLattice[Set[A]] with + def combine(a1: Set[A], a2: Set[A]): Set[A] = + a1 union a2 - val empty: Set[A] = - Set.empty[A] - } -} + val empty: Set[A] = + Set.empty[A] ``` ```scala mdoc:silent -trait GCounter[F[_,_],K, V] { +trait GCounter[F[_,_],K, V]: def increment(f: F[K, V])(k: K, v: V) - (implicit m: CommutativeMonoid[V]): F[K, V] + (using m: CommutativeMonoid[V]): F[K, V] def merge(f1: F[K, V], f2: F[K, V]) - (implicit b: BoundedSemiLattice[V]): F[K, V] + (using b: BoundedSemiLattice[V]): F[K, V] def total(f: F[K, V]) - (implicit m: CommutativeMonoid[V]): V -} + (using m: CommutativeMonoid[V]): V -object GCounter { +object GCounter: def apply[F[_,_], K, V] - (implicit counter: GCounter[F, K, V]) = + (using counter: GCounter[F, K, V]) = counter -} ``` Try defining an instance of this type class for `Map`. @@ -83,34 +73,32 @@ in the companion object for `GCounter` to place it in global implicit scope: ```scala mdoc:silent -import cats.instances.list._ // for Monoid -import cats.instances.map._ // for Monoid -import cats.syntax.semigroup._ // for |+| -import cats.syntax.foldable._ // for combineAll - -implicit def mapGCounterInstance[K, V]: GCounter[Map, K, V] = - new GCounter[Map, K, V] { - def increment(map: Map[K, V])(key: K, value: V) - (implicit m: CommutativeMonoid[V]): Map[K, V] = { - val total = map.getOrElse(key, m.empty) |+| value - map + (key -> total) - } +import cats.instances.list.* // for Monoid +import cats.instances.map.* // for Monoid +import cats.syntax.semigroup.* // for |+| +import cats.syntax.foldable.* // for combineAll + +given mapGCounterInstance[K, V]: GCounter[Map, K, V] with + def increment(map: Map[K, V])(key: K, value: V) + (using m: CommutativeMonoid[V]): Map[K, V] = { + val total = map.getOrElse(key, m.empty) |+| value + map + (key -> total) + } - def merge(map1: Map[K, V], map2: Map[K, V]) - (implicit b: BoundedSemiLattice[V]): Map[K, V] = - map1 |+| map2 + def merge(map1: Map[K, V], map2: Map[K, V]) + (using b: BoundedSemiLattice[V]): Map[K, V] = + map1 |+| map2 - def total(map: Map[K, V]) - (implicit m: CommutativeMonoid[V]): V = - map.values.toList.combineAll - } + def total(map: Map[K, V]) + (using m: CommutativeMonoid[V]): V = + map.values.toList.combineAll ``` You should be able to use your instance as follows: ```scala mdoc:silent -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid val g1 = Map("a" -> 7, "b" -> 3) val g2 = Map("a" -> 2, "b" -> 5) @@ -120,7 +108,7 @@ val counter = GCounter[Map, String, Int] ```scala mdoc val merged = counter.merge(g1, g2) -val total = counter.total(merged) +val total = counter.total(merged)(using intInstance) ``` The implementation strategy @@ -139,7 +127,7 @@ for any type that has a `KeyValueStore` instance. Here's the code for such a type class: ```scala mdoc:silent -trait KeyValueStore[F[_,_]] { +trait KeyValueStore[F[_,_]]: def put[K, V](f: F[K, V])(k: K, v: V): F[K, V] def get[K, V](f: F[K, V])(k: K): Option[V] @@ -148,7 +136,6 @@ trait KeyValueStore[F[_,_]] { get(f)(k).getOrElse(default) def values[K, V](f: F[K, V]): List[V] -} ``` Implement your own instance for `Map`. @@ -160,21 +147,19 @@ the companion object for `KeyValueStore` to place it in global implicit scope: ```scala mdoc:silent -implicit val mapKeyValueStoreInstance: KeyValueStore[Map] = - new KeyValueStore[Map] { - def put[K, V](f: Map[K, V])(k: K, v: V): Map[K, V] = - f + (k -> v) +given mapKeyValueStoreInstance: KeyValueStore[Map] with + def put[K, V](f: Map[K, V])(k: K, v: V): Map[K, V] = + f + (k -> v) - def get[K, V](f: Map[K, V])(k: K): Option[V] = - f.get(k) + def get[K, V](f: Map[K, V])(k: K): Option[V] = + f.get(k) - override def getOrElse[K, V](f: Map[K, V]) - (k: K, default: V): V = - f.getOrElse(k, default) + override def getOrElse[K, V](f: Map[K, V]) + (k: K, default: V): V = + f.getOrElse(k, default) - def values[K, V](f: Map[K, V]): List[V] = - f.values.toList - } + def values[K, V](f: Map[K, V]): List[V] = + f.values.toList ``` @@ -182,19 +167,19 @@ With our type class in place we can implement syntax to enhance data types for which we have instances: ```scala mdoc:silent -implicit class KvsOps[F[_,_], K, V](f: F[K, V]) { +extension [F[_,_], K, V](f: F[K, V]) { def put(key: K, value: V) - (implicit kvs: KeyValueStore[F]): F[K, V] = + (using kvs: KeyValueStore[F]): F[K, V] = kvs.put(f)(key, value) - def get(key: K)(implicit kvs: KeyValueStore[F]): Option[V] = + def get(key: K)(using kvs: KeyValueStore[F]): Option[V] = kvs.get(f)(key) def getOrElse(key: K, default: V) - (implicit kvs: KeyValueStore[F]): V = + (using kvs: KeyValueStore[F]): V = kvs.getOrElse(f)(key, default) - def values(implicit kvs: KeyValueStore[F]): List[V] = + def values(using kvs: KeyValueStore[F]): List[V] = kvs.values(f) } ``` @@ -205,22 +190,19 @@ instances of `KeyValueStore` and `CommutativeMonoid` using an `implicit def`: ```scala mdoc:silent -implicit def gcounterInstance[F[_,_], K, V] - (implicit kvs: KeyValueStore[F], km: CommutativeMonoid[F[K, V]]) = - new GCounter[F, K, V] { +given gcounterInstance[F[_,_], K, V](using kvs: KeyValueStore[F], km: CommutativeMonoid[F[K, V]]): GCounter[F, K, V] with def increment(f: F[K, V])(key: K, value: V) - (implicit m: CommutativeMonoid[V]): F[K, V] = { + (using m: CommutativeMonoid[V]): F[K, V] = { val total = f.getOrElse(key, m.empty) |+| value f.put(key, total) } def merge(f1: F[K, V], f2: F[K, V]) - (implicit b: BoundedSemiLattice[V]): F[K, V] = + (using b: BoundedSemiLattice[V]): F[K, V] = f1 |+| f2 - def total(f: F[K, V])(implicit m: CommutativeMonoid[V]): V = + def total(f: F[K, V])(using m: CommutativeMonoid[V]): V = f.values.combineAll - } ``` The complete code for this case study is quite long, diff --git a/src/pages/case-studies/crdt/g-counter.md b/src/pages/case-studies/crdt/g-counter.md index 6d413517..720c41b0 100644 --- a/src/pages/case-studies/crdt/g-counter.md +++ b/src/pages/case-studies/crdt/g-counter.md @@ -100,7 +100,7 @@ We can implement a GCounter with the following interface, where we represent machine IDs as `Strings`. ```scala mdoc:reset-object:silent -final case class GCounter(counters: Map[String, Int]) { +final case class GCounter(counters: Map[String, Int]): def increment(machine: String, amount: Int) = ??? @@ -109,7 +109,6 @@ final case class GCounter(counters: Map[String, Int]) { def total: Int = ??? -} ``` Finish the implementation! @@ -119,7 +118,7 @@ Hopefully the description above was clear enough that you can get to an implementation like the one below. ```scala mdoc:silent:reset-object -final case class GCounter(counters: Map[String, Int]) { +final case class GCounter(counters: Map[String, Int]): def increment(machine: String, amount: Int) = { val value = amount + counters.getOrElse(machine, 0) GCounter(counters + (machine -> value)) @@ -131,8 +130,6 @@ final case class GCounter(counters: Map[String, Int]) { k -> (v max that.counters.getOrElse(k, 0)) }) - def total: Int = - counters.values.sum -} + def total: Int = counters.values.sum ``` diff --git a/src/pages/case-studies/crdt/generalisation.md b/src/pages/case-studies/crdt/generalisation.md index 1b4fb4d8..cf15bb93 100644 --- a/src/pages/case-studies/crdt/generalisation.md +++ b/src/pages/case-studies/crdt/generalisation.md @@ -113,10 +113,9 @@ our own `BoundedSemiLattice` type class. ```scala mdoc:silent import cats.kernel.CommutativeMonoid -trait BoundedSemiLattice[A] extends CommutativeMonoid[A] { +trait BoundedSemiLattice[A] extends CommutativeMonoid[A]: def combine(a1: A, a2: A): A def empty: A -} ``` In the implementation above, @@ -146,32 +145,26 @@ import cats.kernel.CommutativeMonoid ``` ```scala mdoc:silent -object wrapper { - trait BoundedSemiLattice[A] extends CommutativeMonoid[A] { +object wrapper: + trait BoundedSemiLattice[A] extends CommutativeMonoid[A]: def combine(a1: A, a2: A): A def empty: A - } - object BoundedSemiLattice { - implicit val intInstance: BoundedSemiLattice[Int] = - new BoundedSemiLattice[Int] { - def combine(a1: Int, a2: Int): Int = - a1 max a2 + given intInstance: BoundedSemiLattice[Int] with + def combine(a1: Int, a2: Int): Int = + a1 max a2 - val empty: Int = - 0 - } + val empty: Int = + 0 - implicit def setInstance[A]: BoundedSemiLattice[Set[A]] = - new BoundedSemiLattice[Set[A]]{ - def combine(a1: Set[A], a2: Set[A]): Set[A] = - a1 union a2 + given setInstance[A]: BoundedSemiLattice[Set[A]] with + def combine(a1: Set[A], a2: Set[A]): Set[A] = + a1 union a2 - val empty: Set[A] = - Set.empty[A] - } - } -}; import wrapper._ + val empty: Set[A] = + Set.empty[A] + +import wrapper.* ``` @@ -196,25 +189,24 @@ which significantly simplifies the process of merging and maximising counters: ```scala mdoc:silent -import cats.instances.list._ // for Monoid -import cats.instances.map._ // for Monoid -import cats.syntax.semigroup._ // for |+| -import cats.syntax.foldable._ // for combineAll +import cats.instances.list.* // for Monoid +import cats.instances.map.* // for Monoid +import cats.syntax.semigroup.* // for |+| +import cats.syntax.foldable.* // for combineAll -final case class GCounter[A](counters: Map[String,A]) { +final case class GCounter[A](counters: Map[String,A]): def increment(machine: String, amount: A) - (implicit m: CommutativeMonoid[A]): GCounter[A] = { + (using m: CommutativeMonoid[A]): GCounter[A] = { val value = amount |+| counters.getOrElse(machine, m.empty) GCounter(counters + (machine -> value)) } def merge(that: GCounter[A]) - (implicit b: BoundedSemiLattice[A]): GCounter[A] = + (using b: BoundedSemiLattice[A]): GCounter[A] = GCounter(this.counters |+| that.counters) - def total(implicit m: CommutativeMonoid[A]): A = + def total(using m: CommutativeMonoid[A]): A = this.counters.values.toList.combineAll -} ``` diff --git a/src/pages/case-studies/map-reduce/index.md b/src/pages/case-studies/map-reduce/index.md index 8606c179..2d00d093 100644 --- a/src/pages/case-studies/map-reduce/index.md +++ b/src/pages/case-studies/map-reduce/index.md @@ -161,14 +161,14 @@ Here's some sample output for reference: ```scala mdoc:invisible:reset import cats.Monoid -import cats.syntax.semigroup._ // for |+| +import cats.syntax.semigroup.* // for |+| def foldMap[A, B: Monoid](values: Vector[A])(func: A => B): B = values.foldLeft(Monoid[B].empty)(_ |+| func(_)) ``` ```scala mdoc:silent -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc @@ -176,7 +176,7 @@ foldMap(Vector(1, 2, 3))(identity) ``` ```scala mdoc:silent -import cats.instances.string._ // for Monoid +import cats.instances.string.* // for Monoid ``` ```scala mdoc @@ -194,7 +194,7 @@ as described in Section [@sec:monoid-syntax]: ```scala mdoc:reset:silent import cats.Monoid -import cats.syntax.semigroup._ // for |+| +import cats.syntax.semigroup.* // for |+| def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B = as.map(func).foldLeft(Monoid[B].empty)(_ |+| _) @@ -204,7 +204,7 @@ We can make a slight alteration to this code to do everything in one step: ```scala mdoc:reset:invisible import cats.Monoid -import cats.syntax.semigroup._ +import cats.syntax.semigroup.* ``` ```scala mdoc:silent def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B = @@ -304,9 +304,9 @@ Future.sequence(List(Future(1), Future(2), Future(3))) or an instance of `Traverse`: ```scala mdoc:silent -import cats.instances.future._ // for Applicative -import cats.instances.list._ // for Traverse -import cats.syntax.traverse._ // for sequence +import cats.instances.future.* // for Applicative +import cats.instances.list.* // for Traverse +import cats.syntax.traverse.* // for sequence ``` ```scala mdoc @@ -318,8 +318,8 @@ Finally, we can use `Await.result` to block on a `Future` until a result is available: ```scala mdoc:silent -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* ``` ```scala mdoc @@ -331,8 +331,8 @@ available from `cats.instances.future`: ```scala mdoc:silent import cats.{Monad, Monoid} -import cats.instances.int._ // for Monoid -import cats.instances.future._ // for Monad and Monoid +import cats.instances.int.* // for Monoid +import cats.instances.future.* // for Monad and Monoid Monad[Future].pure(42) @@ -385,10 +385,10 @@ splits out each `map` and `fold` into a separate line of code: ```scala mdoc:invisible:reset -import cats._ -import cats.implicits._ -import scala.concurrent._ -import scala.concurrent.duration._ +import cats.* +import cats.implicits.* +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global ``` ```scala mdoc:silent @@ -432,10 +432,10 @@ are actually equivalent to a single call to `foldMap`, shortening the entire algorithm as follows: ```scala mdoc:reset:invisible -import cats._ -import cats.implicits._ -import scala.concurrent._ -import scala.concurrent.duration._ +import cats.* +import cats.implicits.* +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global def foldMap[A, B : Monoid](as: Vector[A])(func: A => B): B = as.foldLeft(Monoid[B].empty)(_ |+| func(_)) @@ -482,15 +482,15 @@ We'll restate all of the necessary imports for completeness: ```scala mdoc:silent:reset import cats.Monoid -import cats.instances.int._ // for Monoid -import cats.instances.future._ // for Applicative and Monad -import cats.instances.vector._ // for Foldable and Traverse +import cats.instances.int.* // for Monoid +import cats.instances.future.* // for Applicative and Monad +import cats.instances.vector.* // for Foldable and Traverse -import cats.syntax.foldable._ // for combineAll and foldMap -import cats.syntax.traverse._ // for traverse +import cats.syntax.foldable.* // for combineAll and foldMap +import cats.syntax.traverse.* // for traverse -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global ``` diff --git a/src/pages/case-studies/parser/applicative.md b/src/pages/case-studies/parser/applicative.md index 671e622c..4cd99f06 100644 --- a/src/pages/case-studies/parser/applicative.md +++ b/src/pages/case-studies/parser/applicative.md @@ -15,16 +15,14 @@ Given a nested tuple like `((1, 2), 3)` we could write an implicit conversion to ### Exercise: Flattening -Define a `Tuple2Flatten` implicit class with an extension method `flatten`, and an instance of `Flattener` for a nested tuple like `((1, 2), 3)`. +Define an extension method `flatten`, and an instance of `Flattener` for a nested tuple like `((1, 2), 3)`.
~~~ scala -implicit class Tuple2Flatten[A, B, C](val in: ((A, B), C)) extends AnyVal { +extension [A, B, C](in: ((A, B), C)) def flatten: (A, B, C) = - in match { + in match case ((a, b), c) => (a, b, c) - } -} ~~~ It's fairly straightforward to define this class, but we can't abstract over it in any way. If we want to define `flatten` for a tuple like `(a, (b, c))` we need to define a new extension method. Similarly if we want to flatten a tuple with four elements. @@ -107,8 +105,8 @@ The method is called `ap` and types `F[A]` that implement it are called **applic The Scalaz library provides an applicative functor type class, and instances for `Option` and many other types. In Scalaz, `ap` is written as `<*>` for consistency with Haskell. Here's an example: ~~~ scala -import scalaz.syntax.applicative._ -import scalaz.std.option._ +import scalaz.syntax.applicative.* +import scalaz.std.option.* val adder = ((x: Int, y: Int, z: Int) => x + y + z).curried // adder: Int => (Int => (Int => Int)) = @@ -145,13 +143,12 @@ Let's implement an instance of Scalaz's `Applicative` type class for our `Parser Define a typeclass instance of `Applicative` for `Parser`. You must implement the following trait: ~~~ scala -Applicative[Parser] { +Applicative[Parser] with def point[A](a: => A): Parser[A] = ??? def ap[A, B](fa: => Parser[A])(f: => Parser[A => B]): Parser[B] = ??? -} ~~~ Hints: @@ -168,25 +165,24 @@ The usual place to define typeclass instances is as implicit elements on the com val identity: Parser[Unit] = Parser { input => Success(Unit, input) } -implicit object applicativeInstance extends Applicative[Parser] { +given applicativeInstance: Applicative[Parser] with def point[A](a: => A): Parser[A] = identity map (_ => a) def ap[A, B](fa: => Parser[A])(f: => Parser[A => B]): Parser[B] = Parser { input => - f.parse(input) match { + f.parse(input) match case fail @ Failure(_) => fail case Success(aToB, remainder) => - fa.parse(remainder) match { + fa.parse(remainder) match case fail @ Failure(_) => fail case Success(a, remainder1) => Success(aToB(a), remainder1) - } - } + end match + end match } -} ~~~ Checkout the `parser-applicative` tag to see the full code and tests. @@ -198,7 +194,7 @@ Checkout the `parser-applicative` tag to see the full code and tests. Once we have our `Applicative` instance we can take it for a spin: ~~~ scala -import scalaz.syntax.applicative._ +import scalaz.syntax.applicative.* val parser = Parser.string("chicken") <*> ((_: String) => "Tastes like chicken").point[Parser] // parser: underscore.parser.Parser[String] = Parser() @@ -258,7 +254,7 @@ Sometime we do need more than one result, so the problem still remains. In these Here is it in use ~~~ scala -import scalaz.syntax.applicative._ +import scalaz.syntax.applicative.* def taste(taster: String, action: String, flava: String): String = s"$flava tastes like chicken!" // taste: (taster: String, action: String, flava: String)String diff --git a/src/pages/case-studies/parser/error-handling.md b/src/pages/case-studies/parser/error-handling.md index fb6c4b60..3abf3bec 100644 --- a/src/pages/case-studies/parser/error-handling.md +++ b/src/pages/case-studies/parser/error-handling.md @@ -12,18 +12,17 @@ We will fix this by thinking and coding systematically. The first thing is ask o Once we make this realisation the code follows straight-away. For this type of data we use the *sealed trait* pattern. -### The Sealed Trait Pattern +### Enum Pattern If some data `A` can be a `B` or a `C` and nothing else, we should write ~~~ scala -sealed trait A -final case class B() extends A -final case class C() extends A +enum A { + case B() + case C() +} ~~~ -Sealed traits. Extension points (final / non-final) - ### Exercise ### Better ParserResult @@ -35,9 +34,10 @@ Implement a better `ParserResult`. This exercise is deliberately vague about wha Here's my implementation. It follows the existing pattern for success, maintaining the `result` and `remainder` fields, but returns an error message on failure. I also renamed `ParseResult` to just `Parse`. With the subtypes it's fairly obvious that a `Parse` is the result of a `Parser`. ~~~ scala -sealed trait Parse -final case class Failure(message: String) extends Parse -final case class Success(result: String, remainder: String) extends Parse +enum Parse { + case class Failure(message: String) + case class Success(result: String, remainder: String) +} ~~~
@@ -129,11 +129,8 @@ Implement a parser for digits using the regular expression `"[0-9]"` to match a ~~~ scala package underscore.parser -object NumericParser { - +object NumericParser: val digits = Parser.regex("[0-9]").+ - -} ~~~ @@ -184,14 +181,11 @@ If `add` was a normal method we'ld only print `"Hi"` once. If we make the parameter of `~` and `|` call-by-name, our `Parser` will work. Try it and you'll see another issue---the way the grammar is written we'll stop after parsing the first number. (Try `expression.parse("123+456")` and you'll see.) The solution is to rewrite the grammar so we look for compound expressions first and we proceed left-to-right. ~~~ scala -object NumericParser { - +object NumericParser: val digits = Parser.regex("[0-9]").+ def expression: Parser = (digits ~ Parser.string("+") ~ expression) | (digits ~ Parser.string("-") ~ expression) | digits - -} ~~~ The code is tagged with `parser-numeric-expression`. diff --git a/src/pages/case-studies/parser/intro.md b/src/pages/case-studies/parser/intro.md index d6ed4990..dafbdf52 100644 --- a/src/pages/case-studies/parser/intro.md +++ b/src/pages/case-studies/parser/intro.md @@ -32,18 +32,14 @@ package underscore.parser import scala.annotation.tailrec -case class ParseResult(result: String, remainder: String) { - +case class ParseResult(result: String, remainder: String): def failed: Boolean = result.isEmpty def success: Boolean = !failed -} - -case class Parser(parse: String => ParseResult) { - +case class Parser(parse: String => ParseResult): def ~(next: Parser): Parser = ??? def `*`: Parser = @@ -51,20 +47,16 @@ case class Parser(parse: String => ParseResult) { @tailrec def loop(result: String, remainder: String): ParseResult = { val result1 = this.parse(remainder) - result1 match { + result1 match case _ if result1.failed => ParseResult(result, remainder) case ParseResult(result1, remainder1) => loop(result + result1, remainder1) - } } loop("", input) } -} - -object Parser { - +object Parser: def string(literal: String): Parser = Parser { input => if(input.startsWith(literal)) @@ -72,8 +64,6 @@ object Parser { else ParseResult("", input) } - -} ~~~ What does the code do? The first thing is to look at what a `Parser` is. It is basically a wrapper around a function `String => ParseResult`. The `String` parameter is the input to parse, and returned is the result of parsing that `String`. @@ -115,18 +105,18 @@ Checkout the `parser-initial-a` tag to see the complete code with `~` implemente def ~(next: Parser): Parser = Parser { input => val result = this.parse(input) - result match { + result match case _ if result.failed => result case ParseResult(parsed, remainder) => val result1 = next.parse(remainder) - result1 match { + result1 match case _ if result1.failed => ParseResult("", input) case ParseResult(parsed1, remainder1) => ParseResult(parsed + parsed1, remainder1) - } - } + end match + end match } ~~~ diff --git a/src/pages/case-studies/parser/transforms.md b/src/pages/case-studies/parser/transforms.md index 49da1360..be938e28 100644 --- a/src/pages/case-studies/parser/transforms.md +++ b/src/pages/case-studies/parser/transforms.md @@ -101,10 +101,11 @@ Start by implementing data structures to store the result of a successful parse. An expression is an addition, or a substraction, or a number. Once we have this structure its realisation in code is straightforward. ~~~ scala -sealed trait Expression -final case class Addition(left: Expression, right: Expression) extends Expression -final case class Subtraction(left: Expression, right: Expression) extends Expression -final case class Number(value: Int) extends Expression +enum Expression { + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Number(value: Int) +} ~~~ @@ -147,20 +148,10 @@ There are two ways we could write this method: as a method on `Expression` using Either way, `eval` is a straightforward application of structural recursion. My implementation is: ~~~ scala -sealed trait Expression { - def eval: Int -} -final case class Addition(left: Expression, right: Expression) extends Expression { - def eval: Int = - left.eval + right.eval -} -final case class Subtraction(left: Expression, right: Expression) extends Expression { - def eval: Int = - left.eval - right.eval -} -final case class Number(value: Int) extends Expression { - def eval: Int = - value +enum Expression(val eval: Int) { + case Addition(left: Expression, right: Expression) extends Expression(left.eval + right.eval) + case Subtraction(left: Expression, right: Expression) extends Expression(left.eval - right.eval) + case Number(value: Int) extends Expression(value) } ~~~ diff --git a/src/pages/case-studies/testing/index.md b/src/pages/case-studies/testing/index.md index f1d3ba76..2843fa58 100644 --- a/src/pages/case-studies/testing/index.md +++ b/src/pages/case-studies/testing/index.md @@ -15,24 +15,22 @@ that polls remote servers for their uptime: ```scala mdoc:silent import scala.concurrent.Future -trait UptimeClient { +trait UptimeClient: def getUptime(hostname: String): Future[Int] -} ``` We'll also have an `UptimeService` that maintains a list of servers and allows the user to poll them for their total uptime: ```scala mdoc:silent -import cats.instances.future._ // for Applicative -import cats.instances.list._ // for Traverse -import cats.syntax.traverse._ // for traverse +import cats.instances.future.* // for Applicative +import cats.instances.list.* // for Traverse +import cats.syntax.traverse.* // for traverse import scala.concurrent.ExecutionContext.Implicits.global -class UptimeService(client: UptimeClient) { +class UptimeService(client: UptimeClient): def getTotalUptime(hostnames: List[String]): Future[Int] = hostnames.traverse(client.getUptime).map(_.sum) -} ``` We've modelled `UptimeClient` as a trait @@ -42,10 +40,9 @@ that allows us to provide dummy data rather than calling out to actual servers: ```scala mdoc:silent -class TestUptimeClient(hosts: Map[String, Int]) extends UptimeClient { +class TestUptimeClient(hosts: Map[String, Int]) extends UptimeClient: def getUptime(hostname: String): Future[Int] = Future.successful(hosts.getOrElse(hostname, 0)) -} ``` Now, suppose we're writing unit tests for `UptimeService`. @@ -53,7 +50,7 @@ We want to test its ability to sum values, regardless of where it is getting them from. Here's an example: -```scala mdoc:warn +```scala mdoc:fail def testTotalUptime() = { val hosts = Map("host1" -> 10, "host2" -> 6) val client = new TestUptimeClient(hosts) @@ -89,13 +86,11 @@ an asynchronous one for use in production and a synchronous one for use in our unit tests: ```scala -trait RealUptimeClient extends UptimeClient { +trait RealUptimeClient extends UptimeClient: def getUptime(hostname: String): Future[Int] -} -trait TestUptimeClient extends UptimeClient { +trait TestUptimeClient extends UptimeClient: def getUptime(hostname: String): Int -} ``` The question is: what result type should we give @@ -103,9 +98,8 @@ to the abstract method in `UptimeClient`? We need to abstract over `Future[Int]` and `Int`: ```scala -trait UptimeClient { +trait UptimeClient: def getUptime(hostname: String): ??? -} ``` At first this may seem difficult. @@ -145,17 +139,14 @@ import scala.concurrent.Future ```scala mdoc:silent import cats.Id -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} -trait RealUptimeClient extends UptimeClient[Future] { +trait RealUptimeClient extends UptimeClient[Future]: def getUptime(hostname: String): Future[Int] -} -trait TestUptimeClient extends UptimeClient[Id] { +trait TestUptimeClient extends UptimeClient[Id]: def getUptime(hostname: String): Id[Int] -} ``` Note that, because `Id[A]` is just a simple alias for `A`, @@ -166,18 +157,15 @@ as `Id[Int]`---we can simply write `Int` instead: import scala.concurrent.Future import cats.Id -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} -trait RealUptimeClient extends UptimeClient[Future] { +trait RealUptimeClient extends UptimeClient[Future]: def getUptime(hostname: String): Future[Int] -} ``` ```scala mdoc:silent -trait TestUptimeClient extends UptimeClient[Id] { +trait TestUptimeClient extends UptimeClient[Id]: def getUptime(hostname: String): Int -} ``` Of course, technically speaking @@ -200,20 +188,17 @@ the call to `Future.successful`: import scala.concurrent.Future import cats.Id -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} -trait RealUptimeClient extends UptimeClient[Future] { +trait RealUptimeClient extends UptimeClient[Future]: def getUptime(hostname: String): Future[Int] -} ``` ```scala mdoc:silent class TestUptimeClient(hosts: Map[String, Int]) - extends UptimeClient[Id] { + extends UptimeClient[Id]: def getUptime(hostname: String): Int = hosts.getOrElse(hostname, 0) -} ``` @@ -237,11 +222,10 @@ Starting with the method signatures: The code should look like this: ```scala -class UptimeService[F[_]](client: UptimeClient[F]) { +class UptimeService[F[_]](client: UptimeClient[F]): def getTotalUptime(hostnames: List[String]): F[Int] = ??? // hostnames.traverse(client.getUptime).map(_.sum) -} ``` @@ -268,44 +252,40 @@ to `UptimeService`. We can write this as an implicit parameter: ```scala mdoc:invisible:reset-object -import cats.syntax.traverse._ // for traverse -import cats.instances.list._ +import cats.syntax.traverse.* // for traverse +import cats.instances.list.* -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} ``` ```scala mdoc:silent import cats.Applicative -import cats.syntax.functor._ // for map +import cats.syntax.functor.* // for map class UptimeService[F[_]](client: UptimeClient[F]) - (implicit a: Applicative[F]) { + (using a: Applicative[F]): def getTotalUptime(hostnames: List[String]): F[Int] = hostnames.traverse(client.getUptime).map(_.sum) -} ``` or more tersely as a context bound: ```scala mdoc:reset-object:invisible import cats.Applicative -import cats.syntax.functor._ -import cats.syntax.traverse._ -import cats.instances.list._ +import cats.syntax.functor.* +import cats.syntax.traverse.* +import cats.instances.list.* -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} ``` ```scala mdoc:silent class UptimeService[F[_]: Applicative] - (client: UptimeClient[F]) { + (client: UptimeClient[F]): def getTotalUptime(hostnames: List[String]): F[Int] = hostnames.traverse(client.getUptime).map(_.sum) -} ``` Note that we need to import `cats.syntax.functor` @@ -326,29 +306,25 @@ synchronously without worrying about monads or applicatives: ```scala mdoc:invisible:reset-object import cats.{Id, Applicative} -import cats.instances.list._ // for Traverse -import cats.syntax.functor._ // for map -import cats.syntax.traverse._ // for traverse +import cats.instances.list.* // for Traverse +import cats.syntax.functor.* // for map +import cats.syntax.traverse.* // for traverse import scala.concurrent.Future -trait UptimeClient[F[_]] { +trait UptimeClient[F[_]]: def getUptime(hostname: String): F[Int] -} trait RealUptimeClient extends UptimeClient[Future] class TestUptimeClient(hosts: Map[String, Int]) - extends UptimeClient[Id] { + extends UptimeClient[Id]: def getUptime(hostname: String): Int = hosts.getOrElse(hostname, 0) - } class UptimeService[F[_]: Applicative] - (client: UptimeClient[F]) { - + (client: UptimeClient[F]): def getTotalUptime(hostnames: List[String]): F[Int] = hostnames.traverse(client.getUptime).map(_.sum) -} ``` ```scala mdoc:silent def testTotalUptime() = { diff --git a/src/pages/case-studies/validation/check.md b/src/pages/case-studies/validation/check.md index a9d89c3a..ed3ddf99 100644 --- a/src/pages/case-studies/validation/check.md +++ b/src/pages/case-studies/validation/check.md @@ -28,11 +28,10 @@ We will probably want to add custom methods to `Check` so let's declare it as a `trait` instead of a type alias: ```scala mdoc:silent:reset-object -trait Check[E, A] { +trait Check[E, A]: def apply(value: A): Either[E, A] // other methods... -} ``` As we said in [Essential Scala][link-essential-scala], @@ -56,12 +55,11 @@ Think about implementing this method now. You should hit some problems. Read on when you do! ```scala mdoc:silent:reset-object -trait Check[E, A] { +trait Check[E, A]: def and(that: Check[E, A]): Check[E, A] = ??? // other methods... -} ``` The problem is: what do you do when *both* checks fail? @@ -83,8 +81,8 @@ the `combine` method or its associated `|+|` syntax: ```scala mdoc:silent import cats.Semigroup -import cats.instances.list._ // for Semigroup -import cats.syntax.semigroup._ // for |+| +import cats.instances.list.* // for Semigroup +import cats.syntax.semigroup.* // for |+| val semigroup = Semigroup[List[String]] ``` @@ -131,17 +129,17 @@ we'll call this implementation `CheckF`: ```scala mdoc:silent import cats.Semigroup -import cats.syntax.either._ // for asLeft and asRight -import cats.syntax.semigroup._ // for |+| +import cats.syntax.either.* // for asLeft and asRight +import cats.syntax.semigroup.* // for |+| ``` ```scala mdoc:silent -final case class CheckF[E, A](func: A => Either[E, A]) { +final case class CheckF[E, A](func: A => Either[E, A]): def apply(a: A): Either[E, A] = func(a) def and(that: CheckF[E, A]) - (implicit s: Semigroup[E]): CheckF[E, A] = + (using s: Semigroup[E]): CheckF[E, A] = CheckF { a => (this(a), that(a)) match { case (Left(e1), Left(e2)) => (e1 |+| e2).asLeft @@ -150,14 +148,13 @@ final case class CheckF[E, A](func: A => Either[E, A]) { case (Right(_), Right(_)) => a.asRight } } -} ``` Let's test the behaviour we get. First we'll setup some checks: ```scala mdoc:silent -import cats.instances.list._ // for Semigroup +import cats.instances.list.* // for Semigroup val a: CheckF[List[String], Int] = CheckF { v => @@ -193,14 +190,14 @@ What happens if we create instances of `CheckF[Nothing, A]`? ```scala mdoc:invisible:reset-object import cats.Semigroup -import cats.syntax.semigroup._ -import cats.syntax.either._ -final case class CheckF[E, A](func: A => Either[E, A]) { +import cats.syntax.semigroup.* +import cats.syntax.either.* +final case class CheckF[E, A](func: A => Either[E, A]): def apply(a: A): Either[E, A] = func(a) def and(that: CheckF[E, A]) - (implicit s: Semigroup[E]): CheckF[E, A] = + (using s: Semigroup[E]): CheckF[E, A] = CheckF { a => (this(a), that(a)) match { case (Left(e1), Left(e2)) => (e1 |+| e2).asLeft @@ -209,7 +206,6 @@ final case class CheckF[E, A](func: A => Either[E, A]) { case (Right(_), Right(_)) => a.asRight } } -} ``` ```scala mdoc:silent val a: CheckF[Nothing, Int] = @@ -235,19 +231,19 @@ We'll call this implementation `Check`: ```scala mdoc:invisible:reset-object import cats.Semigroup -import cats.syntax.either._ // for asLeft and asRight -import cats.syntax.semigroup._ // for |+| +import cats.syntax.either.* // for asLeft and asRight +import cats.syntax.semigroup.* // for |+| ``` ```scala mdoc:silent -sealed trait Check[E, A] { - import Check._ +sealed trait Check[E, A]: + import Check.* def and(that: Check[E, A]): Check[E, A] = And(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Either[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Either[E, A] = + this match case Pure(func) => func(a) @@ -258,9 +254,9 @@ sealed trait Check[E, A] { case (Right(_), Left(e)) => e.asLeft case (Right(_), Right(_)) => a.asRight } - } -} -object Check { + end match + +object Check: final case class And[E, A]( left: Check[E, A], right: Check[E, A]) extends Check[E, A] @@ -270,7 +266,6 @@ object Check { def pure[E, A](f: A => Either[E, A]): Check[E, A] = Pure(f) -} ``` Let's see an example: @@ -332,33 +327,31 @@ Here's the complete implementation: ```scala mdoc:silent:reset-object import cats.Semigroup import cats.data.Validated -import cats.syntax.apply._ // for mapN +import cats.syntax.apply.* // for mapN ``` ```scala mdoc:silent -sealed trait Check[E, A] { - import Check._ +sealed trait Check[E, A]: + import Check.* def and(that: Check[E, A]): Check[E, A] = And(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) case And(left, right) => (left(a), right(a)).mapN((_, _) => a) - } -} -object Check { + +object Check: final case class And[E, A]( left: Check[E, A], right: Check[E, A]) extends Check[E, A] final case class Pure[E, A]( func: A => Validated[E, A]) extends Check[E, A] -} ``` @@ -375,14 +368,14 @@ is implicit in the semantics of "or". ```scala mdoc:silent:reset-object import cats.Semigroup import cats.data.Validated -import cats.syntax.semigroup._ // for |+| -import cats.syntax.apply._ // for mapN -import cats.data.Validated._ // for Valid and Invalid +import cats.syntax.semigroup.* // for |+| +import cats.syntax.apply.* // for mapN +import cats.data.Validated.* // for Valid and Invalid ``` ```scala mdoc:silent -sealed trait Check[E, A] { - import Check._ +sealed trait Check[E, A]: + import Check.* def and(that: Check[E, A]): Check[E, A] = And(this, that) @@ -390,8 +383,8 @@ sealed trait Check[E, A] { def or(that: Check[E, A]): Check[E, A] = Or(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) @@ -407,9 +400,9 @@ sealed trait Check[E, A] { case Invalid(e2) => Invalid(e1 |+| e2) } } - } -} -object Check { + end match + +object Check: final case class And[E, A]( left: Check[E, A], right: Check[E, A]) extends Check[E, A] @@ -420,7 +413,6 @@ object Check { final case class Pure[E, A]( func: A => Validated[E, A]) extends Check[E, A] -} ``` diff --git a/src/pages/case-studies/validation/kleisli.md b/src/pages/case-studies/validation/kleisli.md index 91d0421e..a26e3d8b 100644 --- a/src/pages/case-studies/validation/kleisli.md +++ b/src/pages/case-studies/validation/kleisli.md @@ -65,7 +65,7 @@ through three steps: ```scala mdoc:silent import cats.data.Kleisli -import cats.instances.list._ // for Monad +import cats.instances.list.* // for Monad ``` These steps each transform an input `Int` @@ -125,15 +125,14 @@ Like `apply`, the method must accept an implicit `Semigroup`: import cats.Semigroup import cats.data.Validated -sealed trait Predicate[E, A] { - def run(implicit s: Semigroup[E]): A => Either[E, A] = +sealed trait Predicate[E, A]: + def run(using s: Semigroup[E]): A => Either[E, A] = (a: A) => this(a).toEither def apply(a: A): Validated[E, A] = ??? // etc... // other methods... -} ``` @@ -172,13 +171,13 @@ def checkPred[A](pred: Predicate[Errors, A]): Check[A, A] = // Foreword declarations import cats.Semigroup -import cats.syntax.apply._ // for mapN -import cats.syntax.semigroup._ // for |+| +import cats.syntax.apply.* // for mapN +import cats.syntax.semigroup.* // for |+| import cats.data.Validated import cats.data.Validated.{Valid, Invalid} -sealed trait Predicate[E, A] { - import Predicate._ +sealed trait Predicate[E, A]: + import Predicate.* def and(that: Predicate[E, A]): Predicate[E, A] = And(this, that) @@ -186,11 +185,11 @@ sealed trait Predicate[E, A] { def or(that: Predicate[E, A]): Predicate[E, A] = Or(this, that) - def run(implicit s: Semigroup[E]): A => Either[E, A] = + def run(using s: Semigroup[E]): A => Either[E, A] = (a: A) => this(a).toEither - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) @@ -206,10 +205,10 @@ sealed trait Predicate[E, A] { case Invalid(e2) => Invalid(e1 |+| e2) } } - } -} + end match +end Predicate -object Predicate { +object Predicate: final case class And[E, A]( left: Predicate[E, A], right: Predicate[E, A]) extends Predicate[E, A] @@ -226,7 +225,7 @@ object Predicate { def lift[E, A](error: E, func: A => Boolean): Predicate[E, A] = Pure(a => if(func(a)) Valid(a) else Invalid(error)) -} +end Predicate ``` Working around limitations of type inference @@ -237,7 +236,7 @@ simplifies things, but the process is still complex: ```scala mdoc:silent import cats.data.{Kleisli, NonEmptyList} -import cats.instances.either._ // for Semigroupal +import cats.instances.either.* // for Semigroupal ``` Here is the preamble we suggested in @@ -330,7 +329,7 @@ def createUser( email: String): Either[Errors, User] = ( checkUsername.run(username), checkEmail.run(email) -).mapN(User) +).mapN(User.apply) ``` ```scala mdoc diff --git a/src/pages/case-studies/validation/map.md b/src/pages/case-studies/validation/map.md index 88c4a703..c911044b 100644 --- a/src/pages/case-studies/validation/map.md +++ b/src/pages/case-studies/validation/map.md @@ -89,21 +89,21 @@ Making this change gives us the following code: ```scala mdoc:silent import cats.Semigroup import cats.data.Validated -import cats.syntax.semigroup._ // for |+| -import cats.syntax.apply._ // for mapN -import cats.data.Validated._ // for Valid and Invalid +import cats.syntax.semigroup.* // for |+| +import cats.syntax.apply.* // for mapN +import cats.data.Validated.* // for Valid and Invalid ``` ```scala mdoc:silent -sealed trait Predicate[E, A] { +sealed trait Predicate[E, A]: def and(that: Predicate[E, A]): Predicate[E, A] = And(this, that) def or(that: Predicate[E, A]): Predicate[E, A] = Or(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) @@ -119,8 +119,7 @@ sealed trait Predicate[E, A] { case Invalid(e2) => Invalid(e1 |+| e2) } } - } -} + end match final case class And[E, A]( left: Predicate[E, A], @@ -142,13 +141,12 @@ that also allows transformation of its input. Implement `Check` with the following interface: ```scala -sealed trait Check[E, A, B] { +sealed trait Check[E, A, B]: def apply(a: A): Validated[E, B] = ??? def map[C](func: B => C): Check[E, A, C] = ??? -} ```
@@ -158,11 +156,11 @@ you should be able to create code similar to the below: ```scala mdoc:invisible:reset-object import cats.Semigroup import cats.data.Validated -import cats.implicits._ +import cats.implicits.* -sealed trait Predicate[E, A] { - import Predicate._ - import Validated._ +sealed trait Predicate[E, A]: + import Predicate.* + import Validated.* def and(that: Predicate[E, A]): Predicate[E, A] = And(this, that) @@ -170,8 +168,8 @@ sealed trait Predicate[E, A] { def or(that: Predicate[E, A]): Predicate[E, A] = Or(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) @@ -187,9 +185,9 @@ sealed trait Predicate[E, A] { case Invalid(e2) => Invalid(e1 |+| e2) } } - } -} -object Predicate { + end match + +object Predicate: final case class And[E, A]( left: Predicate[E, A], right: Predicate[E, A]) extends Predicate[E, A] @@ -200,7 +198,6 @@ object Predicate { final case class Pure[E, A]( func: A => Validated[E, A]) extends Predicate[E, A] -} ``` ```scala mdoc:silent import cats.Semigroup @@ -208,35 +205,32 @@ import cats.data.Validated ``` ```scala mdoc:silent -sealed trait Check[E, A, B] { - import Check._ +sealed trait Check[E, A, B]: + import Check.* - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, B] + def apply(in: A)(using s: Semigroup[E]): Validated[E, B] def map[C](f: B => C): Check[E, A, C] = Map[E, A, B, C](this, f) -} -object Check { +object Check: final case class Map[E, A, B, C]( check: Check[E, A, B], - func: B => C) extends Check[E, A, C] { + func: B => C) extends Check[E, A, C]: - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, C] = + def apply(in: A)(using s: Semigroup[E]): Validated[E, C] = check(in).map(func) - } + end Map final case class Pure[E, A]( - pred: Predicate[E, A]) extends Check[E, A, A] { + pred: Predicate[E, A]) extends Check[E, A, A]: - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, A] = + def apply(in: A)(using s: Semigroup[E]): Validated[E, A] = pred(in) - } + end Pure def apply[E, A](pred: Predicate[E, A]): Check[E, A, A] = Pure(pred) -} - ```
@@ -288,22 +282,20 @@ import cats.data.Validated ``` ```scala mdoc:silent -sealed trait Check[E, A, B] { - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, B] +sealed trait Check[E, A, B]: + def apply(in: A)(using s: Semigroup[E]): Validated[E, B] def flatMap[C](f: B => Check[E, A, C]) = FlatMap[E, A, B, C](this, f) // other methods... -} final case class FlatMap[E, A, B, C]( check: Check[E, A, B], - func: B => Check[E, A, C]) extends Check[E, A, C] { + func: B => Check[E, A, C]) extends Check[E, A, C]: - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, C] = + def apply(a: A)(using s: Semigroup[E]): Validated[E, C] = check(a).withEither(_.flatMap(b => func(b)(a).toEither)) -} // other data types... ``` @@ -325,9 +317,8 @@ A `Check` is basically a function `A => Validated[E, B]` so we can define an analagous `andThen` method: ```scala -trait Check[E, A, B] { +trait Check[E, A, B]: def andThen[C](that: Check[E, B, C]): Check[E, A, C] -} ``` Implement `andThen` now! @@ -341,20 +332,18 @@ import cats.Semigroup import cats.data.Validated ``` ```scala mdoc:silent -sealed trait Check[E, A, B] { - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, B] +sealed trait Check[E, A, B]: + def apply(in: A)(using s: Semigroup[E]): Validated[E, B] def andThen[C](that: Check[E, B, C]): Check[E, A, C] = AndThen[E, A, B, C](this, that) -} final case class AndThen[E, A, B, C]( check1: Check[E, A, B], - check2: Check[E, B, C]) extends Check[E, A, C] { + check2: Check[E, B, C]) extends Check[E, A, C]: - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, C] = + def apply(a: A)(using s: Semigroup[E]): Validated[E, C] = check1(a).withEither(_.flatMap(b => check2(b).toEither)) -} ``` @@ -373,10 +362,10 @@ including some tidying and repackaging of the code: ```scala mdoc:silent:reset-object import cats.Semigroup import cats.data.Validated -import cats.data.Validated._ // for Valid and Invalid -import cats.syntax.semigroup._ // for |+| -import cats.syntax.apply._ // for mapN -import cats.syntax.validated._ // for valid and invalid +import cats.data.Validated.* // for Valid and Invalid +import cats.syntax.semigroup.* // for |+| +import cats.syntax.apply.* // for mapN +import cats.syntax.validated.* // for valid and invalid ``` Here is our complete implementation of `Predicate`, @@ -385,9 +374,9 @@ a `Predicate.apply` method to create a `Predicate` from a function: ```scala mdoc:silent -sealed trait Predicate[E, A] { - import Predicate._ - import Validated._ +sealed trait Predicate[E, A]: + import Predicate.* + import Validated.* def and(that: Predicate[E, A]): Predicate[E, A] = And(this, that) @@ -395,8 +384,8 @@ sealed trait Predicate[E, A] { def or(that: Predicate[E, A]): Predicate[E, A] = Or(this, that) - def apply(a: A)(implicit s: Semigroup[E]): Validated[E, A] = - this match { + def apply(a: A)(using s: Semigroup[E]): Validated[E, A] = + this match case Pure(func) => func(a) @@ -412,10 +401,9 @@ sealed trait Predicate[E, A] { case Invalid(e2) => Invalid(e1 |+| e2) } } - } -} + end match -object Predicate { +object Predicate: final case class And[E, A]( left: Predicate[E, A], right: Predicate[E, A]) extends Predicate[E, A] @@ -432,7 +420,6 @@ object Predicate { def lift[E, A](err: E, fn: A => Boolean): Predicate[E, A] = Pure(a => if(fn(a)) a.valid else err.invalid) -} ``` Here is a complete implementation of `Check`. @@ -444,14 +431,14 @@ using inheritance: ```scala mdoc:silent import cats.Semigroup import cats.data.Validated -import cats.syntax.apply._ // for mapN -import cats.syntax.validated._ // for valid and invalid +import cats.syntax.apply.* // for mapN +import cats.syntax.validated.* // for valid and invalid ``` ```scala mdoc:silent -sealed trait Check[E, A, B] { - import Check._ +sealed trait Check[E, A, B]: + import Check.* - def apply(in: A)(implicit s: Semigroup[E]): Validated[E, B] + def apply(in: A)(using s: Semigroup[E]): Validated[E, B] def map[C](f: B => C): Check[E, A, C] = Map[E, A, B, C](this, f) @@ -461,51 +448,50 @@ sealed trait Check[E, A, B] { def andThen[C](next: Check[E, B, C]): Check[E, A, C] = AndThen[E, A, B, C](this, next) -} -object Check { +object Check: final case class Map[E, A, B, C]( check: Check[E, A, B], - func: B => C) extends Check[E, A, C] { + func: B => C) extends Check[E, A, C]: def apply(a: A) - (implicit s: Semigroup[E]): Validated[E, C] = + (using s: Semigroup[E]): Validated[E, C] = check(a) map func - } + end Map final case class FlatMap[E, A, B, C]( check: Check[E, A, B], - func: B => Check[E, A, C]) extends Check[E, A, C] { + func: B => Check[E, A, C]) extends Check[E, A, C]: def apply(a: A) - (implicit s: Semigroup[E]): Validated[E, C] = + (using s: Semigroup[E]): Validated[E, C] = check(a).withEither(_.flatMap(b => func(b)(a).toEither)) - } + end FlatMap final case class AndThen[E, A, B, C]( check: Check[E, A, B], - next: Check[E, B, C]) extends Check[E, A, C] { + next: Check[E, B, C]) extends Check[E, A, C]: def apply(a: A) - (implicit s: Semigroup[E]): Validated[E, C] = + (using s: Semigroup[E]): Validated[E, C] = check(a).withEither(_.flatMap(b => next(b).toEither)) - } + end AndThen final case class Pure[E, A, B]( - func: A => Validated[E, B]) extends Check[E, A, B] { + func: A => Validated[E, B]) extends Check[E, A, B]: def apply(a: A) - (implicit s: Semigroup[E]): Validated[E, B] = + (using s: Semigroup[E]): Validated[E, B] = func(a) - } + end Pure final case class PurePredicate[E, A]( - pred: Predicate[E, A]) extends Check[E, A, A] { + pred: Predicate[E, A]) extends Check[E, A, A]: def apply(a: A) - (implicit s: Semigroup[E]): Validated[E, A] = + (using s: Semigroup[E]): Validated[E, A] = pred(a) - } + end PurePredicate def apply[E, A](pred: Predicate[E, A]): Check[E, A, A] = PurePredicate(pred) @@ -513,7 +499,6 @@ object Check { def apply[E, A, B] (func: A => Validated[E, B]): Check[E, A, B] = Pure(func) -} ``` @@ -589,8 +574,8 @@ In later sections we'll make some changes that make the library easier to use. ```scala mdoc:silent -import cats.syntax.apply._ // for mapN -import cats.syntax.validated._ // for valid and invalid +import cats.syntax.apply.* // for mapN +import cats.syntax.validated.* // for valid and invalid ``` Here's the implementation of `checkUsername`: @@ -614,14 +599,14 @@ built up from a number of smaller components: // at least three characters long and contain a dot. val splitEmail: Check[Errors, String, (String, String)] = - Check(_.split('@') match { + Check(_.split('@') match case Array(name, domain) => (name, domain).validNel[String] case _ => "Must contain a single @ character". invalidNel[(String, String)] - }) + ) val checkLeft: Check[Errors, String, String] = Check(longerThan(0)) @@ -630,7 +615,7 @@ val checkRight: Check[Errors, String, String] = Check(longerThan(3) and contains('.')) val joinEmail: Check[Errors, (String, String), String] = - Check { case (l, r) => + Check { (l, r) => (checkLeft(l), checkRight(r)).mapN(_ + "@" + _) } @@ -647,7 +632,7 @@ final case class User(username: String, email: String) def createUser( username: String, email: String): Validated[Errors, User] = - (checkUsername(username), checkEmail(email)).mapN(User) + (checkUsername(username), checkEmail(email)).mapN(User.apply) ``` We can check our work by creating diff --git a/src/pages/foldable-traverse/foldable-cats.md b/src/pages/foldable-traverse/foldable-cats.md index 2116e54c..fdec8fc0 100644 --- a/src/pages/foldable-traverse/foldable-cats.md +++ b/src/pages/foldable-traverse/foldable-cats.md @@ -13,7 +13,7 @@ Here is an example using `List`: ```scala mdoc:silent import cats.Foldable -import cats.instances.list._ // for Foldable +import cats.instances.list.* // for Foldable val ints = List(1, 2, 3) ``` @@ -27,7 +27,7 @@ Here is an example using `Option`, which is treated like a sequence of zero or one elements: ```scala mdoc:silent -import cats.instances.option._ // for Foldable +import cats.instances.option.* // for Foldable val maybeInt = Option(123) ``` @@ -73,7 +73,7 @@ Using `Foldable` forces us to use stack safe operations, which fixes the overflow exception: ```scala mdoc:silent -import cats.instances.lazyList._ // for Foldable +import cats.instances.lazyList.* // for Foldable ``` ```scala mdoc:silent @@ -130,7 +130,7 @@ Cats provides two methods that make use of `Monoids`: For example, we can use `combineAll` to sum over a `List[Int]`: ```scala mdoc:silent -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc @@ -141,7 +141,7 @@ Alternatively, we can use `foldMap` to convert each `Int` to a `String` and concatenate them: ```scala mdoc:silent -import cats.instances.string._ // for Monoid +import cats.instances.string.* // for Monoid ``` ```scala mdoc @@ -153,12 +153,12 @@ to support deep traversal of nested sequences: ```scala mdoc:invisible:reset-object import cats.Foldable -import cats.instances.list._ -import cats.instances.int._ -import cats.instances.string._ +import cats.instances.list.* +import cats.instances.int.* +import cats.instances.string.* ``` ```scala mdoc:silent -import cats.instances.vector._ // for Monoid +import cats.instances.vector.* // for Monoid val ints = List(Vector(1, 2, 3), Vector(4, 5, 6)) ``` @@ -175,7 +175,7 @@ In each case, the first argument to the method on `Foldable` becomes the receiver of the method call: ```scala mdoc:silent -import cats.syntax.foldable._ // for combineAll and foldMap +import cats.syntax.foldable.* // for combineAll and foldMap ``` ```scala mdoc diff --git a/src/pages/foldable-traverse/foldable.md b/src/pages/foldable-traverse/foldable.md index 576aef44..42307f42 100644 --- a/src/pages/foldable-traverse/foldable.md +++ b/src/pages/foldable-traverse/foldable.md @@ -141,7 +141,7 @@ one using `scala.math.Numeric` import scala.math.Numeric def sumWithNumeric[A](list: List[A]) - (implicit numeric: Numeric[A]): A = + (using numeric: Numeric[A]): A = list.foldRight(numeric.zero)(numeric.plus) ``` @@ -156,10 +156,10 @@ and one using `cats.Monoid` import cats.Monoid def sumWithMonoid[A](list: List[A]) - (implicit monoid: Monoid[A]): A = + (using monoid: Monoid[A]): A = list.foldRight(monoid.empty)(monoid.combine) -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc diff --git a/src/pages/foldable-traverse/traverse-cats.md b/src/pages/foldable-traverse/traverse-cats.md index b4a78c72..21d456ad 100644 --- a/src/pages/foldable-traverse/traverse-cats.md +++ b/src/pages/foldable-traverse/traverse-cats.md @@ -10,14 +10,13 @@ Here's the abbreviated definition: ```scala package cats -trait Traverse[F[_]] { +trait Traverse[F[_]]: def traverse[G[_]: Applicative, A, B] (inputs: F[A])(func: A => G[B]): G[F[B]] def sequence[G[_]: Applicative, B] (inputs: F[G[B]]): G[F[B]] = traverse(inputs)(identity) -} ``` Cats provides instances of `Traverse` @@ -28,8 +27,8 @@ and use the `traverse` and `sequence` methods as described in the previous section: ```scala mdoc:invisible -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global val hostnames = List( @@ -44,8 +43,8 @@ def getUptime(hostname: String): Future[Int] = ```scala mdoc:silent import cats.Traverse -import cats.instances.future._ // for Applicative -import cats.instances.list._ // for Traverse +import cats.instances.future.* // for Applicative +import cats.instances.list.* // for Traverse val totalUptime: Future[List[Int]] = Traverse[List].traverse(hostnames)(getUptime) @@ -70,12 +69,12 @@ There are also syntax versions of the methods, imported via [`cats.syntax.traverse`][cats.syntax.traverse]: ```scala mdoc:silent -import cats.syntax.traverse._ // for sequence and traverse +import cats.syntax.traverse.* // for sequence and traverse ``` ```scala mdoc -Await.result(hostnames.traverse(getUptime), 1.second) -Await.result(numbers.sequence, 1.second) +Await.result(hostnames.traverse[Future, Int](getUptime), 1.second) +Await.result(numbers.sequence[Future, Int], 1.second) ``` As you can see, this is much more compact and readable diff --git a/src/pages/foldable-traverse/traverse.md b/src/pages/foldable-traverse/traverse.md index 500abc1d..ba5518e9 100644 --- a/src/pages/foldable-traverse/traverse.md +++ b/src/pages/foldable-traverse/traverse.md @@ -16,8 +16,8 @@ As an example, suppose we have a list of server hostnames and a method to poll a host for its uptime: ```scala mdoc:silent -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global val hostnames = List( @@ -62,8 +62,8 @@ We can improve on things greatly using `Future.traverse`, which is tailor-made for this pattern: ```scala mdoc:invisible:reset-object -import scala.concurrent._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global val hostnames = List( @@ -113,12 +113,11 @@ that assumes we're starting with a `List[Future[B]]` and don't need to provide an identity function: ```scala -object Future { +object Future: def sequence[B](futures: List[Future[B]]): Future[List[B]] = traverse(futures)(identity) // etc... -} ``` In this case the intuitive understanding is even simpler: @@ -157,8 +156,8 @@ is equivalent to `Applicative.pure`: ```scala mdoc:silent import cats.Applicative -import cats.instances.future._ // for Applicative -import cats.syntax.applicative._ // for pure +import cats.instances.future.* // for Applicative +import cats.syntax.applicative.* // for pure List.empty[Int].pure[Future] ``` @@ -181,7 +180,7 @@ def oldCombine( is now equivalent to `Semigroupal.combine`: ```scala mdoc:silent -import cats.syntax.apply._ // for mapN +import cats.syntax.apply.* // for mapN // Combining accumulator and hostname using an Applicative: def newCombine(accum: Future[List[Int]], @@ -223,7 +222,7 @@ as shown in the following exercises. What is the result of the following? ```scala mdoc:silent -import cats.instances.vector._ // for Applicative +import cats.instances.vector.* // for Applicative listSequence(List(Vector(1, 2), Vector(3, 4))) ``` @@ -263,7 +262,7 @@ listSequence(List(Vector(1, 2), Vector(3, 4), Vector(5, 6))) Here's an example that uses `Options`: ```scala mdoc:silent -import cats.instances.option._ // for Applicative +import cats.instances.option.* // for Applicative def process(inputs: List[Int]) = listTraverse(inputs)(n => if(n % 2 == 0) Some(n) else None) @@ -297,8 +296,8 @@ Finally, here is an example that uses `Validated`: ```scala mdoc:invisible:reset import cats.Applicative -import cats.syntax.applicative._ // for pure -import cats.syntax.apply._ // for mapN +import cats.syntax.applicative.* // for pure +import cats.syntax.apply.* // for mapN def listTraverse[F[_]: Applicative, A, B] (list: List[A])(func: A => F[B]): F[List[B]] = list.foldLeft(List.empty[B].pure[F]) { (accum, item) => @@ -307,7 +306,7 @@ def listTraverse[F[_]: Applicative, A, B] ``` ```scala mdoc:silent import cats.data.Validated -import cats.instances.list._ // for Monoid +import cats.instances.list.* // for Monoid type ErrorsOr[A] = Validated[List[String], A] diff --git a/src/pages/functors/cats.md b/src/pages/functors/cats.md index 3ae4835c..a3cd5cc2 100644 --- a/src/pages/functors/cats.md +++ b/src/pages/functors/cats.md @@ -14,8 +14,8 @@ the [`cats.instances`][cats.instances] package: ```scala mdoc:silent:reset-object import cats.Functor -import cats.instances.list._ // for Functor -import cats.instances.option._ // for Functor +import cats.instances.list.* // for Functor +import cats.instances.option.* // for Functor ``` ```scala mdoc @@ -60,8 +60,8 @@ Scala's `Function1` type doesn't have a `map` method so there are no naming conflicts: ```scala mdoc:silent -import cats.instances.function._ // for Functor -import cats.syntax.functor._ // for map +import cats.instances.function.* // for Functor +import cats.syntax.functor.* // for map ``` ```scala mdoc:silent @@ -83,11 +83,11 @@ no matter what functor context it's in: ```scala mdoc:silent def doMath[F[_]](start: F[Int]) - (implicit functor: Functor[F]): F[Int] = + (using functor: Functor[F]): F[Int] = start.map(n => n + 1 * 2) -import cats.instances.option._ // for Functor -import cats.instances.list._ // for Functor +import cats.instances.option.* // for Functor +import cats.instances.list.* // for Functor ``` ```scala mdoc @@ -101,11 +101,10 @@ the `map` method in `cats.syntax.functor`. Here's a simplified version of the code: ```scala -implicit class FunctorOps[F[_], A](src: F[A]) { +extension [F[_], A](src: F[A]) def map[B](func: A => B) - (implicit functor: Functor[F]): F[B] = + (using functor: Functor[F]): F[B] = functor.map(src)(func) -} ``` The compiler can use this extension method @@ -115,16 +114,8 @@ to insert a `map` method wherever no built-in `map` is available: foo.map(value => value + 1) ``` -Assuming `foo` has no built-in `map` method, -the compiler detects the potential error and -wraps the expression in a `FunctorOps` to fix the code: - -```scala -new FunctorOps(foo).map(value => value + 1) -``` - -The `map` method of `FunctorOps` requires -an implicit `Functor` as a parameter. +The `map` extension method requires +a using clause of `Functor` as a parameter. This means this code will only compile if we have a `Functor` for `F` in scope. If we don't, we get a compiler error: @@ -153,35 +144,30 @@ even though such a thing already exists in [`cats.instances`][cats.instances]. The implementation is trivial---we simply call `Option's` `map` method: ```scala mdoc:silent -implicit val optionFunctor: Functor[Option] = - new Functor[Option] { - def map[A, B](value: Option[A])(func: A => B): Option[B] = - value.map(func) - } +given optionFunctor: Functor[Option] with + def map[A, B](value: Option[A])(func: A => B): Option[B] = + value.map(func) ``` Sometimes we need to inject dependencies into our instances. For example, if we had to define a custom `Functor` for `Future` (another hypothetical example---Cats provides one in `cats.instances.future`) -we would need to account for the implicit `ExecutionContext` parameter on `future.map`. +we would need to account for the using clause `ExecutionContext` parameter on `future.map`. We can't add extra parameters to `functor.map` so we have to account for the dependency when we create the instance: ```scala mdoc:silent import scala.concurrent.{Future, ExecutionContext} -implicit def futureFunctor - (implicit ec: ExecutionContext): Functor[Future] = - new Functor[Future] { - def map[A, B](value: Future[A])(func: A => B): Future[B] = - value.map(func) - } +given futureFunctor(using ec: ExecutionContext): Functor[Future] with + def map[A, B](value: Future[A])(func: A => B): Future[B] = + value.map(func) ``` Whenever we summon a `Functor` for `Future`, either directly using `Functor.apply` or indirectly via the `map` extension method, -the compiler will locate `futureFunctor` by implicit resolution +the compiler will locate `futureFunctor` by given instance resolution and recursively search for an `ExecutionContext` at the call site. This is what the expansion might look like: @@ -190,10 +176,10 @@ This is what the expansion might look like: Functor[Future] // The compiler expands to this first: -Functor[Future](futureFunctor) +Functor[Future](using futureFunctor) // And then to this: -Functor[Future](futureFunctor(executionContext)) +Functor[Future](using futureFunctor(using executionContext)) ``` ### Exercise: Branching out with Functors @@ -202,12 +188,9 @@ Write a `Functor` for the following binary tree data type. Verify that the code works as expected on instances of `Branch` and `Leaf`: ```scala mdoc:silent -sealed trait Tree[+A] - -final case class Branch[A](left: Tree[A], right: Tree[A]) - extends Tree[A] - -final case class Leaf[A](value: A) extends Tree[A] +enum Tree[+A]: + case Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A] + case Leaf[A](value: A) extends Tree[A] ```
@@ -218,17 +201,15 @@ with the same pattern of `Branch` and `Leaf` nodes: ```scala mdoc:silent import cats.Functor +import Tree.{Branch, Leaf} -implicit val treeFunctor: Functor[Tree] = - new Functor[Tree] { - def map[A, B](tree: Tree[A])(func: A => B): Tree[B] = - tree match { - case Branch(left, right) => - Branch(map(left)(func), map(right)(func)) - case Leaf(value) => - Leaf(func(value)) - } - } +given treeFunctor: Functor[Tree] with + def map[A, B](tree: Tree[A])(func: A => B): Tree[B] = + tree match + case Branch(left, right) => + Branch(map(left)(func), map(right)(func)) + case Leaf(value) => + Leaf(func(value)) ``` Let's use our `Functor` to transform some `Trees`: @@ -243,13 +224,12 @@ The compiler can find a `Functor` instance for `Tree` but not for `Branch` or `L Let's add some smart constructors to compensate: ```scala mdoc:silent -object Tree { +object Tree: def branch[A](left: Tree[A], right: Tree[A]): Tree[A] = Branch(left, right) def leaf[A](value: A): Tree[A] = Leaf(value) -} ``` Now we can use our `Functor` properly: diff --git a/src/pages/functors/contravariant-invariant-cats.md b/src/pages/functors/contravariant-invariant-cats.md index 71341612..1bf2e37d 100644 --- a/src/pages/functors/contravariant-invariant-cats.md +++ b/src/pages/functors/contravariant-invariant-cats.md @@ -10,13 +10,11 @@ Here's a simplified version of the code: ``` ```scala mdoc:silent -trait Contravariant[F[_]] { +trait Contravariant[F[_]]: def contramap[A, B](fa: F[A])(f: B => A): F[B] -} -trait Invariant[F[_]] { +trait Invariant[F[_]]: def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] -} ``` ### Contravariant in Cats @@ -30,7 +28,7 @@ Here's an example: ```scala mdoc:silent:reset import cats.Contravariant import cats.Show -import cats.instances.string._ +import cats.instances.string.* val showString = Show[String] @@ -47,7 +45,7 @@ More conveniently, we can use which provides a `contramap` extension method: ```scala mdoc:silent -import cats.syntax.contravariant._ // for contramap +import cats.syntax.contravariant.* // for contramap ``` ```scala mdoc @@ -67,10 +65,9 @@ If you recall, this is what `Monoid` looks like: ```scala package cats -trait Monoid[A] { +trait Monoid[A]: def empty: A def combine(x: A, y: A): A -} ``` Imagine we want to produce a `Monoid` @@ -95,11 +92,11 @@ provided by `cats.syntax.invariant`: ```scala mdoc:silent import cats.Monoid -import cats.instances.string._ // for Monoid -import cats.syntax.invariant._ // for imap -import cats.syntax.semigroup._ // for |+| +import cats.instances.string.* // for Monoid +import cats.syntax.invariant.* // for imap +import cats.syntax.semigroup.* // for |+| -implicit val symbolMonoid: Monoid[Symbol] = +given symbolMonoid: Monoid[Symbol] = Monoid[String].imap(Symbol.apply)(_.name) ``` diff --git a/src/pages/functors/contravariant-invariant.md b/src/pages/functors/contravariant-invariant.md index 30b07dd0..5831dbe2 100644 --- a/src/pages/functors/contravariant-invariant.md +++ b/src/pages/functors/contravariant-invariant.md @@ -38,9 +38,8 @@ However, we can define `contramap` for the `Printable` type class we discussed in Chapter [@sec:type-classes]: ```scala mdoc:silent -trait Printable[A] { +trait Printable[A]: def format(value: A): String -} ``` A `Printable[A]` represents a transformation from `A` to `String`. @@ -48,14 +47,13 @@ Its `contramap` method accepts a function `func` of type `B => A` and creates a new `Printable[B]`: ```scala mdoc:silent:reset-object -trait Printable[A] { +trait Printable[A]: def format(value: A): String def contramap[B](func: B => A): Printable[B] = ??? -} -def format[A](value: A)(implicit p: Printable[A]): String = +def format[A](value: A)(using p: Printable[A]): String = p.format(value) ``` @@ -66,15 +64,13 @@ Start with the following code template and replace the `???` with a working method body: ```scala -trait Printable[A] { +trait Printable[A]: def format(value: A): String def contramap[B](func: B => A): Printable[B] = - new Printable[B] { + new Printable[B]: def format(value: B): String = ??? - } -} ``` If you get stuck, think about the types. @@ -92,18 +88,16 @@ we use a `self` alias to distinguish the outer and inner `Printables`: ```scala mdoc:silent:reset-object -trait Printable[A] { self => - - def format(value: A): String +trait Printable[A]: + self => + def format(value: A): String - def contramap[B](func: B => A): Printable[B] = - new Printable[B] { - def format(value: B): String = - self.format(func(value)) - } -} + def contramap[B](func: B => A): Printable[B] = + new Printable[B]: + def format(value: B): String = + self.format(func(value)) -def format[A](value: A)(implicit p: Printable[A]): String = +def format[A](value: A)(using p: Printable[A]): String = p.format(value) ```
@@ -113,17 +107,13 @@ let's define some instances of `Printable` for `String` and `Boolean`: ```scala mdoc:silent -implicit val stringPrintable: Printable[String] = - new Printable[String] { - def format(value: String): String = - s"'${value}'" - } - -implicit val booleanPrintable: Printable[Boolean] = - new Printable[Boolean] { - def format(value: Boolean): String = - if(value) "yes" else "no" - } +given stringPrintable: Printable[String] with + def format(value: String): String = + s"'${value}'" + +given booleanPrintable: Printable[Boolean] with + def format(value: Boolean): String = + if(value) "yes" else "no" ``` ```scala mdoc @@ -133,7 +123,7 @@ format(true) Now define an instance of `Printable` for the following `Box` case class. -You'll need to write this as an `implicit def` +You'll need to write this as a `given` instance as described in Section [@sec:type-classes:recursive-implicits]: ```scala mdoc:silent @@ -147,7 +137,7 @@ create your instance from an existing instance using `contramap`. ```scala mdoc:invisible -implicit def boxPrintable[A](implicit p: Printable[A]): Printable[Box[A]] = +given boxPrintable[A](using p: Printable[A]): Printable[Box[A]] = p.contramap[Box[A]](_.value) ``` @@ -171,63 +161,49 @@ we base it on the `Printable` for the type inside the `Box`. We can either write out the complete definition by hand: ```scala mdoc:invisible:reset-object -trait Printable[A] { +trait Printable[A]: self => + def format(value: A): String - def format(value: A): String + def contramap[B](func: B => A): Printable[B] = + (value: B) => self.format(func(value)) - def contramap[B](func: B => A): Printable[B] = - new Printable[B] { - def format(value: B): String = - self.format(func(value)) - } -} final case class Box[A](value: A) ``` ```scala mdoc:silent -implicit def boxPrintable[A]( - implicit p: Printable[A] -): Printable[Box[A]] = - new Printable[Box[A]] { - def format(box: Box[A]): String = - p.format(box.value) - } +given boxPrintable[A](using p: Printable[A]): Printable[Box[A]] with + def format(box: Box[A]): String = + p.format(box.value) ``` or use `contramap` to base the new instance on the implicit parameter: ```scala mdoc:invisible:reset-object -trait Printable[A] { +trait Printable[A]: self => + def format(value: A): String - def format(value: A): String + def contramap[B](func: B => A): Printable[B] = + new Printable[B]: + def format(value: B): String = + self.format(func(value)) - def contramap[B](func: B => A): Printable[B] = - new Printable[B] { - def format(value: B): String = - self.format(func(value)) - } -} - -def format[A](value: A)(implicit p: Printable[A]): String = +def format[A](value: A)(using p: Printable[A]): String = p.format(value) -implicit val stringPrintable: Printable[String] = - new Printable[String] { - def format(value: String): String = - s"'${value}'" - } - -implicit val booleanPrintable: Printable[Boolean] = - new Printable[Boolean] { - def format(value: Boolean): String = - if(value) "yes" else "no" - } +given stringPrintable: Printable[String] with + def format(value: String): String = + s"'${value}'" + +given booleanPrintable: Printable[Boolean] with + def format(value: Boolean): String = + if(value) "yes" else "no" + final case class Box[A](value: A) ``` ```scala mdoc:silent -implicit def boxPrintable[A](implicit p: Printable[A]): Printable[Box[A]] = +given boxPrintable[A](using p: Printable[A]): Printable[Box[A]] = p.contramap[Box[A]](_.value) ``` @@ -257,36 +233,32 @@ We can build our own `Codec` by enhancing `Printable` to support encoding and decoding to/from a `String`: ```scala mdoc:silent -trait Codec[A] { +trait Codec[A]: def encode(value: A): String def decode(value: String): A def imap[B](dec: A => B, enc: B => A): Codec[B] = ??? -} ``` ```scala mdoc:invisible:reset-object -trait Codec[A] { +trait Codec[A]: self => + def encode(value: A): String + def decode(value: String): A - def encode(value: A): String - def decode(value: String): A - - def imap[B](dec: A => B, enc: B => A): Codec[B] = - new Codec[B] { - def encode(value: B): String = - self.encode(enc(value)) + def imap[B](dec: A => B, enc: B => A): Codec[B] = + new Codec[B]: + def encode(value: B): String = + self.encode(enc(value)) - def decode(value: String): B = - dec(self.decode(value)) - } -} + def decode(value: String): B = + dec(self.decode(value)) ``` ```scala mdoc:silent -def encode[A](value: A)(implicit c: Codec[A]): String = +def encode[A](value: A)(using c: Codec[A]): String = c.encode(value) -def decode[A](value: String)(implicit c: Codec[A]): A = +def decode[A](value: String)(using c: Codec[A]): A = c.decode(value) ``` @@ -303,21 +275,19 @@ whose `encode` and `decode` methods both simply return the value they are passed: ```scala mdoc:silent -implicit val stringCodec: Codec[String] = - new Codec[String] { - def encode(value: String): String = value - def decode(value: String): String = value - } +given stringCodec: Codec[String] with + def encode(value: String): String = value + def decode(value: String): String = value ``` We can construct many useful `Codecs` for other types by building off of `stringCodec` using `imap`: ```scala mdoc:silent -implicit val intCodec: Codec[Int] = +given intCodec: Codec[Int] = stringCodec.imap(_.toInt, _.toString) -implicit val booleanCodec: Codec[Boolean] = +given booleanCodec: Codec[Boolean] = stringCodec.imap(_.toBoolean, _.toString) ``` @@ -344,39 +314,35 @@ Implement the `imap` method for `Codec` above. Here's a working implementation: ```scala mdoc:silent:reset-object -trait Codec[A] { self => - def encode(value: A): String - def decode(value: String): A +trait Codec[A]: + self => + def encode(value: A): String + def decode(value: String): A - def imap[B](dec: A => B, enc: B => A): Codec[B] = { - new Codec[B] { - def encode(value: B): String = - self.encode(enc(value)) + def imap[B](dec: A => B, enc: B => A): Codec[B] = + new Codec[B]: + def encode(value: B): String = + self.encode(enc(value)) - def decode(value: String): B = - dec(self.decode(value)) - } - } -} + def decode(value: String): B = + dec(self.decode(value)) ``` ```scala mdoc:invisible -implicit val stringCodec: Codec[String] = - new Codec[String] { - def encode(value: String): String = value - def decode(value: String): String = value - } +given stringCodec: Codec[String] with + def encode(value: String): String = value + def decode(value: String): String = value -implicit val intCodec: Codec[Int] = +given intCodec: Codec[Int] = stringCodec.imap[Int](_.toInt, _.toString) -implicit val booleanCodec: Codec[Boolean] = +given booleanCodec: Codec[Boolean] = stringCodec.imap[Boolean](_.toBoolean, _.toString) -def encode[A](value: A)(implicit c: Codec[A]): String = +def encode[A](value: A)(using c: Codec[A]): String = c.encode(value) -def decode[A](value: String)(implicit c: Codec[A]): A = +def decode[A](value: String)(using c: Codec[A]): A = c.decode(value) ``` @@ -389,7 +355,7 @@ We can implement this using the `imap` method of `stringCodec`: ```scala mdoc:silent -implicit val doubleCodec: Codec[Double] = +given doubleCodec: Codec[Double] = stringCodec.imap[Double](_.toDouble, _.toString) ``` @@ -406,7 +372,7 @@ We create this by calling `imap` on a `Codec[A]`, which we bring into scope using an implicit parameter: ```scala mdoc:silent -implicit def boxCodec[A](implicit c: Codec[A]): Codec[Box[A]] = +given boxCodec[A](using c: Codec[A]): Codec[Box[A]] = c.imap[Box[A]](Box(_), _.value) ``` diff --git a/src/pages/functors/index.md b/src/pages/functors/index.md index 267b0a30..87704802 100644 --- a/src/pages/functors/index.md +++ b/src/pages/functors/index.md @@ -102,7 +102,7 @@ seen in `List`, `Option`, and `Either`: ```scala mdoc:silent import scala.concurrent.{Future, Await} import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import scala.concurrent.duration.* val future: Future[String] = Future(123). @@ -138,19 +138,19 @@ val future1 = { // the next random number in the sequence: val x = Future(r.nextInt()) - for { + for a <- x b <- x - } yield (a, b) + yield (a, b) } val future2 = { val r = new Random(0L) - for { + for a <- Future(r.nextInt()) b <- Future(r.nextInt()) - } yield (a, b) + yield (a, b) } ``` @@ -210,8 +210,8 @@ We also see this in Figure [@fig:functors:function-type-chart]: In other words, "mapping" over a `Function1` is function composition: ```scala mdoc:silent -import cats.instances.function._ // for Functor -import cats.syntax.functor._ // for map +import cats.instances.function.* // for Functor +import cats.syntax.functor.* // for map ``` ```scala mdoc:silent @@ -300,9 +300,8 @@ package cats ```scala mdoc:silent -trait Functor[F[_]] { +trait Functor[F[_]]: def map[A, B](fa: F[A])(f: A => B): F[B] -} ``` If you haven't seen syntax like `F[_]` before, diff --git a/src/pages/functors/partial-unification.md b/src/pages/functors/partial-unification.md index 57b3ad93..cb4a9599 100644 --- a/src/pages/functors/partial-unification.md +++ b/src/pages/functors/partial-unification.md @@ -5,8 +5,8 @@ we saw a functor instance for `Function1`. ```scala mdoc:silent import cats.Functor -import cats.instances.function._ // for Functor -import cats.syntax.functor._ // for map +import cats.instances.function.* // for Functor +import cats.syntax.functor.* // for map val func1 = (x: Int) => x.toDouble val func2 = (y: Double) => y * 2 @@ -19,17 +19,15 @@ val func3 = func1.map(func2) (the function argument and the result type): ```scala -trait Function1[-A, +B] { +trait Function1[-A, +B]: def apply(arg: A): B -} ``` However, `Functor` accepts a type constructor with one parameter: ```scala -trait Functor[F[_]] { +trait Functor[F[_]]: def map[A, B](fa: F[A])(func: A => B): F[B] -} ``` The compiler has to fix one of the two parameters @@ -123,7 +121,7 @@ If we try this for real, however, our code won't compile: ```scala mdoc:silent -import cats.syntax.contravariant._ // for contramap +import cats.syntax.contravariant.* // for contramap ``` ```scala mdoc:fail diff --git a/src/pages/intro/what-is-fp.md b/src/pages/intro/what-is-fp.md index 1a735e20..43fc434f 100644 --- a/src/pages/intro/what-is-fp.md +++ b/src/pages/intro/what-is-fp.md @@ -30,7 +30,7 @@ In my view functional programming is not about immutability, or keeping to "the ```scala mdoc:silent def sum(numbers: List[Int]): Int = { var total = 0 - numbers.foreach(x => total = total + x) + numbers foreach { x => total = total + x } total } ``` @@ -50,7 +50,7 @@ val it2 = Iterator(1, 2, 3, 4) ``` ```scala mdoc -it.zip(it2).next() +(it zip it2).next() ``` However if we pass the same generator twice we get a surprising result. @@ -60,7 +60,7 @@ val it3 = Iterator(1, 2, 3, 4) ``` ```scala mdoc -it3.zip(it3).next() +(it3 zip it3).next() ``` The usual functional programming solution is to avoid mutable state but we can envisage other possibilities. For example, an [effect tracking system][effect-system] would allow us to avoid combining two generators that use the same memory region. These systems are still research projects, however. diff --git a/src/pages/links.md b/src/pages/links.md index 48909519..bd3baf76 100644 --- a/src/pages/links.md +++ b/src/pages/links.md @@ -113,6 +113,7 @@ [link-iterator-pattern]: https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf [link-json]: http://json.org/ [link-kind-projector]: https://github.com/typelevel/kind-projector +[link-kind-projector-migration]: https://docs.scala-lang.org/scala3/guides/migration/plugin-kind-projector.html [link-map-reduce-monoid]: http://arxiv.org/abs/1304.7544 [link-map-reduce]: http://research.google.com/archive/map-reduce.html [link-mdoc]: https://github.com/scalameta/mdoc @@ -122,7 +123,6 @@ [link-phil-freeman-tailrecm]: http://functorial.com/stack-safety-for-free/index.pdf [link-play-json-format]: https://www.playframework.com/documentation/2.6.x/ScalaJsonCombinators#Format [link-sbt]: http://www.scala-sbt.org/ -[link-sbt-catalysts]: https://github.com/typelevel/sbt-catalysts [link-scalactic]: http://scalactic.org [link-scalaz-contrib]: https://github.com/typelevel/scalaz-contrib [link-scodec-codec]: http://scodec.org/guide/Core+Algebra.html#Codec diff --git a/src/pages/monad-transformers/index.md b/src/pages/monad-transformers/index.md index cf92a0ce..fd4ae570 100644 --- a/src/pages/monad-transformers/index.md +++ b/src/pages/monad-transformers/index.md @@ -46,7 +46,7 @@ That is, do monads *compose*? We can try to write the code but we soon hit problems: ```scala mdoc:silent -import cats.syntax.applicative._ // for pure +import cats.syntax.applicative.* // for pure ``` ```scala @@ -120,8 +120,8 @@ using the `OptionT` constructor, or more conveniently using `pure`: ```scala mdoc:silent -import cats.instances.list._ // for Monad -import cats.syntax.applicative._ // for pure +import cats.instances.list.* // for Monad +import cats.syntax.applicative.* // for pure ``` ```scala mdoc @@ -246,7 +246,7 @@ In other words, we build monad stacks from the inside out: ```scala mdoc:invisible:reset import cats.data.OptionT -import cats.syntax.applicative._ // for pure +import cats.syntax.applicative.* // for pure ``` ```scala mdoc:silent type ListOption[A] = OptionT[List, A] @@ -277,7 +277,7 @@ We can use `pure`, `map`, and `flatMap` as usual to create and transform instances: ```scala mdoc:silent -import cats.instances.either._ // for Monad +import cats.instances.either.* // for Monad ``` ```scala mdoc @@ -297,9 +297,8 @@ However, we can't define this in one line because `EitherT` has three type parameters: ```scala -case class EitherT[F[_], E, A](stack: F[Either[E, A]]) { +case class EitherT[F[_], E, A](stack: F[Either[E, A]]): // etc... -} ``` The three type parameters are as follows: @@ -325,10 +324,10 @@ and our `map` and `flatMap` methods cut through three layers of abstraction: ```scala mdoc:silent -import cats.instances.future._ // for Monad +import cats.instances.future.* // for Monad import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import scala.concurrent.duration.* ``` ```scala mdoc:silent @@ -351,7 +350,7 @@ to make it easier to define partially applied type constructors. For example: ```scala mdoc -import cats.instances.option._ // for Monad +import cats.instances.option.* // for Monad 123.pure[EitherT[Option, String, *]] ``` @@ -452,7 +451,7 @@ and use a fusion `Future` and `Either` everywhere in our code: ```scala mdoc:invisible:reset-object import cats.data.EitherT -import cats.instances.list._ +import cats.instances.list.* import scala.concurrent.Future ``` ```scala mdoc:silent @@ -482,10 +481,9 @@ type Logged[A] = Writer[List[String], A] // Methods generally return untransformed stacks: def parseNumber(str: String): Logged[Option[Int]] = - util.Try(str.toInt).toOption match { + util.Try(str.toInt).toOption match case Some(num) => Writer(List(s"Read $str"), Some(num)) case None => Writer(List(s"Failed on $str"), None) - } // Consumers use monad transformers locally to simplify composition: def addAll(a: String, b: String, c: String): Logged[Option[Int]] = { @@ -589,7 +587,7 @@ val powerLevels = Map( ) ``` ```scala mdoc:silent -import cats.instances.future._ // for Monad +import cats.instances.future.* // for Monad import scala.concurrent.ExecutionContext.Implicits.global type Response[A] = EitherT[Future, String, A] @@ -621,8 +619,8 @@ We request the power level from each ally and use `map` and `flatMap` to combine the results: ```scala mdoc:invisible:reset-object -import cats.implicits._ -import cats.data._ +import cats.implicits.* +import cats.data.* import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -664,8 +662,8 @@ We use the `value` method to unpack the monad stack and `Await` and `fold` to unpack the `Future` and `Either`: ```scala mdoc:invisible:reset -import cats.implicits._ -import cats.data._ +import cats.implicits.* +import cats.data.* import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -687,7 +685,7 @@ def getPowerLevel(ally: String): Response[Int] = { ```scala mdoc:silent import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import scala.concurrent.duration.* def canSpecialMove(ally1: String, ally2: String): Response[Boolean] = for { diff --git a/src/pages/monads/cats.md b/src/pages/monads/cats.md index 97f9129c..d2272f8b 100644 --- a/src/pages/monads/cats.md +++ b/src/pages/monads/cats.md @@ -18,8 +18,8 @@ Here are some examples using `pure` and `flatMap`, and `map` directly: ```scala mdoc:silent import cats.Monad -import cats.instances.option._ // for Monad -import cats.instances.list._ // for Monad +import cats.instances.option.* // for Monad +import cats.instances.list.* // for Monad ``` ```scala mdoc @@ -43,7 +43,7 @@ Cats provides instances for all the monads in the standard library (`Option`, `List`, `Vector` and so on) via [`cats.instances`][cats.instances]: ```scala mdoc:silent -import cats.instances.option._ // for Monad +import cats.instances.option.* // for Monad ``` ```scala mdoc @@ -51,7 +51,7 @@ Monad[Option].flatMap(Option(1))(a => Option(a*2)) ``` ```scala mdoc:silent -import cats.instances.list._ // for Monad +import cats.instances.list.* // for Monad ``` ```scala mdoc @@ -59,7 +59,7 @@ Monad[List].flatMap(List(1, 2, 3))(a => List(a, a*10)) ``` ```scala mdoc:silent -import cats.instances.vector._ // for Monad +import cats.instances.vector.* // for Monad ``` ```scala mdoc @@ -75,9 +75,9 @@ To work around this, Cats requires us to have an `ExecutionContext` in scope when we summon a `Monad` for `Future`: ```scala mdoc:silent -import cats.instances.future._ // for Monad -import scala.concurrent._ -import scala.concurrent.duration._ +import cats.instances.future.* // for Monad +import scala.concurrent.* +import scala.concurrent.duration.* ``` ```scala mdoc:fail @@ -129,9 +129,9 @@ We can use `pure` to construct instances of a monad. We'll often need to specify the type parameter to disambiguate the particular instance we want. ```scala mdoc:silent -import cats.instances.option._ // for Monad -import cats.instances.list._ // for Monad -import cats.syntax.applicative._ // for pure +import cats.instances.option.* // for Monad +import cats.instances.list.* // for Monad +import cats.syntax.applicative.* // for pure ``` ```scala mdoc @@ -148,14 +148,14 @@ that come wrapped in a monad of the user's choice: ```scala mdoc:silent import cats.Monad -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatMap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatMap def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = a.flatMap(x => b.map(y => x*x + y*y)) -import cats.instances.option._ // for Monad -import cats.instances.list._ // for Monad +import cats.instances.option.* // for Monad +import cats.instances.list.* // for Monad ``` ```scala mdoc @@ -170,7 +170,7 @@ and inserting the correct implicit conversions to use our `Monad`: ```scala mdoc:invisible:reset-object import cats.Monad -import cats.implicits._ +import cats.implicits.* ``` ```scala mdoc:silent def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = diff --git a/src/pages/monads/custom-instances.md b/src/pages/monads/custom-instances.md index e1c005ce..21cf1411 100644 --- a/src/pages/monads/custom-instances.md +++ b/src/pages/monads/custom-instances.md @@ -47,7 +47,7 @@ and many monads have some notion of stopping. We can write this method in terms of `flatMap`. ```scala mdoc:silent -import cats.syntax.flatMap._ // For flatMap +import cats.syntax.flatMap.* // For flatMap def retry[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] = f(start).flatMap{ a => @@ -59,7 +59,7 @@ Unfortunately it is not stack-safe. It works for small input. ```scala mdoc -import cats.instances.option._ +import cats.instances.option.* retry(100)(a => if(a == 0) None else Some(a - 1)) ``` @@ -75,7 +75,7 @@ We can instead rewrite this method using `tailRecM`. ```scala mdoc:silent -import cats.syntax.functor._ // for map +import cats.syntax.functor.* // for map def retryTailRecM[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] = Monad[F].tailRecM(start){ a => @@ -104,7 +104,7 @@ in terms of `iterateWhileM` and we don't have to explicitly call `tailRecM`. ```scala mdoc:silent -import cats.syntax.monad._ // for iterateWhileM +import cats.syntax.monad.* // for iterateWhileM def retryM[F[_]: Monad, A](start: A)(f: A => F[A]): F[A] = start.iterateWhileM(f)(a => true) @@ -127,12 +127,12 @@ Let's write a `Monad` for our `Tree` data type from last chapter. Here's the type again: ```scala mdoc:silent -sealed trait Tree[+A] - -final case class Branch[A](left: Tree[A], right: Tree[A]) - extends Tree[A] +enum Tree[+A] { + case Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A] + case Leaf[A](value: A) extends Tree[A] +} -final case class Leaf[A](value: A) extends Tree[A] +import Tree.{Branch, Leaf} def branch[A](left: Tree[A], right: Tree[A]): Tree[A] = Branch(left, right) @@ -167,28 +167,26 @@ the non-tail-recursive solution falls out: ```scala mdoc:silent import cats.Monad -implicit val treeMonad = new Monad[Tree] { +given treeMonad: Monad[Tree] with def pure[A](value: A): Tree[A] = Leaf(value) def flatMap[A, B](tree: Tree[A]) (func: A => Tree[B]): Tree[B] = - tree match { + tree match case Branch(l, r) => Branch(flatMap(l)(func), flatMap(r)(func)) case Leaf(value) => func(value) - } - def tailRecM[A, B](a: A) - (func: A => Tree[Either[A, B]]): Tree[B] = - flatMap(func(a)) { - case Left(value) => - tailRecM(value)(func) - case Right(value) => - Leaf(value) - } -} + def tailRecM[A, B](a: A) + (func: A => Tree[Either[A, B]]): Tree[B] = + flatMap(func(a)) { + case Left(value) => + tailRecM(value)(func) + case Right(value) => + Leaf(value) + } ``` The solution above is perfectly fine for this exercise. @@ -219,26 +217,26 @@ def leaf[A](value: A): Tree[A] = import cats.Monad import scala.annotation.tailrec -implicit val treeMonad = new Monad[Tree] { +given treeMonad: Monad[Tree] with def pure[A](value: A): Tree[A] = Leaf(value) def flatMap[A, B](tree: Tree[A]) (func: A => Tree[B]): Tree[B] = - tree match { + tree match case Branch(l, r) => Branch(flatMap(l)(func), flatMap(r)(func)) case Leaf(value) => func(value) - } - def tailRecM[A, B](arg: A) - (func: A => Tree[Either[A, B]]): Tree[B] = { + def tailRecM[A, B](arg: A)( + func: A => Tree[Either[A, B]] + ): Tree[B] = { @tailrec def loop( open: List[Tree[Either[A, B]]], closed: List[Option[Tree[B]]]): List[Tree[B]] = - open match { + open match case Branch(l, r) :: next => loop(l :: r :: next, None :: closed) @@ -255,19 +253,18 @@ implicit val treeMonad = new Monad[Tree] { branch(left, right) :: tail } } - } + end match loop(List(func(arg)), Nil).head } -} ``` Regardless of which version of `tailRecM` we define, we can use our `Monad` to `flatMap` and `map` on `Trees`: ```scala mdoc:silent -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatMap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatMap ``` ```scala mdoc diff --git a/src/pages/monads/either.md b/src/pages/monads/either.md index 8990016e..69738d14 100644 --- a/src/pages/monads/either.md +++ b/src/pages/monads/either.md @@ -49,7 +49,7 @@ In Scala 2.12+ we can either omit this import or leave it in place without breaking anything: ```scala mdoc:silent -import cats.syntax.either._ // for map and flatMap +import cats.syntax.either.* // for map and flatMap for { a <- either1 @@ -64,7 +64,7 @@ we can also import the `asLeft` and `asRight` extension methods from [`cats.syntax.either`][cats.syntax.either]: ```scala mdoc:silent -import cats.syntax.either._ // for asRight +import cats.syntax.either.* // for asRight ``` ```scala mdoc @@ -154,7 +154,7 @@ can use `orElse` and `getOrElse` to extract values from the right side or return a default: ```scala mdoc:silent -import cats.syntax.either._ +import cats.syntax.either.* ``` ```scala mdoc @@ -235,17 +235,15 @@ Another approach is to define an algebraic data type to represent errors that may occur in our program: ```scala mdoc:silent -object wrapper { - sealed trait LoginError extends Product with Serializable +object wrapper: + enum LoginError: + case UserNotFound(username: String) + case PasswordIncorrect(username: String) + case UnexpectedError - final case class UserNotFound(username: String) - extends LoginError +import wrapper.* - final case class PasswordIncorrect(username: String) - extends LoginError - - case object UnexpectedError extends LoginError -}; import wrapper._ +import LoginError.* ``` ```scala mdoc:silent @@ -263,7 +261,7 @@ on any pattern matching we do: ```scala mdoc:silent // Choose error-handling behaviour based on type: def handleError(error: LoginError): Unit = - error match { + error match case UserNotFound(u) => println(s"User not found: $u") @@ -272,7 +270,6 @@ def handleError(error: LoginError): Unit = case UnexpectedError => println(s"Unexpected error") - } ``` ```scala mdoc diff --git a/src/pages/monads/eval.md b/src/pages/monads/eval.md index d4a0dd7d..0a33e23b 100644 --- a/src/pages/monads/eval.md +++ b/src/pages/monads/eval.md @@ -310,12 +310,11 @@ Make it so using `Eval`: ```scala mdoc:silent def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): B = - as match { + as match case head :: tail => fn(head, foldRight(tail, acc)(fn)) case Nil => acc - } ```
@@ -330,12 +329,11 @@ import cats.Eval def foldRightEval[A, B](as: List[A], acc: Eval[B]) (fn: (A, Eval[B]) => Eval[B]): Eval[B] = - as match { + as match case head :: tail => Eval.defer(fn(head, foldRightEval(tail, acc)(fn))) case Nil => acc - } ``` We can redefine `foldRight` simply in terms of `foldRightEval` diff --git a/src/pages/monads/id.md b/src/pages/monads/id.md index 617770b3..f2425e69 100644 --- a/src/pages/monads/id.md +++ b/src/pages/monads/id.md @@ -5,8 +5,8 @@ by writing a method that abstracted over different monads: ```scala mdoc:silent import cats.Monad -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatMap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatMap def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = for { @@ -69,8 +69,8 @@ val b = Monad[Id].flatMap(a)(_ + 1) ``` ```scala mdoc:silent -import cats.syntax.functor._ // for map -import cats.syntax.flatMap._ // for flatMap +import cats.syntax.functor.* // for map +import cats.syntax.flatMap.* // for flatMap ``` ```scala mdoc @@ -116,8 +116,8 @@ All we have to do is return the initial value: ```scala mdoc:invisible:reset-object import cats.{Id,Monad} -import cats.syntax.functor._ -import cats.syntax.flatMap._ +import cats.syntax.functor.* +import cats.syntax.flatMap.* def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = for { x <- a diff --git a/src/pages/monads/index.md b/src/pages/monads/index.md index 3cdf61a7..edb7958a 100644 --- a/src/pages/monads/index.md +++ b/src/pages/monads/index.md @@ -257,11 +257,10 @@ Here is a simplified version of the `Monad` type class in Cats: ```scala mdoc:silent -trait Monad[F[_]] { +trait Monad[F[_]]: def pure[A](value: A): F[A] def flatMap[A, B](value: F[A])(func: A => F[B]): F[B] -} ```
@@ -302,14 +301,13 @@ using the existing methods, `flatMap` and `pure`: ```scala mdoc:silent:reset-object -trait Monad[F[_]] { +trait Monad[F[_]]: def pure[A](a: A): F[A] def flatMap[A, B](value: F[A])(func: A => F[B]): F[B] def map[A, B](value: F[A])(func: A => B): F[B] = ??? -} ``` Try defining `map` yourself now. @@ -322,14 +320,13 @@ Given the tools available there's only one thing we can do: call `flatMap`: ```scala -trait Monad[F[_]] { +trait Monad[F[_]]: def pure[A](value: A): F[A] def flatMap[A, B](value: F[A])(func: A => F[B]): F[B] def map[A, B](value: F[A])(func: A => B): F[B] = flatMap(value)(a => ???) -} ``` We need a function of type `A => F[B]` as the second parameter. @@ -341,13 +338,12 @@ Combining these gives us our result: ```scala mdoc:invisible:reset-object ``` ```scala mdoc -trait Monad[F[_]] { +trait Monad[F[_]]: def pure[A](value: A): F[A] def flatMap[A, B](value: F[A])(func: A => F[B]): F[B] def map[A, B](value: F[A])(func: A => B): F[B] = flatMap(value)(a => pure(func(a))) -} ```
diff --git a/src/pages/monads/monad-error.md b/src/pages/monads/monad-error.md index ba5c46bc..a657f437 100644 --- a/src/pages/monads/monad-error.md +++ b/src/pages/monads/monad-error.md @@ -28,7 +28,7 @@ of the definition of `MonadError`: ```scala package cats -trait MonadError[F[_], E] extends Monad[F] { +trait MonadError[F[_], E] extends Monad[F]: // Lift an error into the `F` context: def raiseError[A](e: E): F[A] @@ -41,7 +41,6 @@ trait MonadError[F[_], E] extends Monad[F] { // Test an instance of `F`, // failing if the predicate is not satisfied: def ensure[A](fa: F[A])(e: E)(f: A => Boolean): F[A] -} ``` `MonadError` is defined in terms of two type parameters: @@ -55,7 +54,7 @@ instantiate the type class for `Either`: ```scala mdoc:silent import cats.MonadError -import cats.instances.either._ // for MonadError +import cats.instances.either.* // for MonadError type ErrorOr[A] = Either[String, A] @@ -126,14 +125,14 @@ and `ensure` via [`cats.syntax.monadError`][cats.syntax.monadError]: ```scala mdoc:invisible:reset import cats.MonadError -import cats.instances.either._ // for MonadError +import cats.instances.either.* // for MonadError type ErrorOr[A] = Either[String, A] ``` ```scala mdoc:silent -import cats.syntax.applicative._ // for pure -import cats.syntax.applicativeError._ // for raiseError etc -import cats.syntax.monadError._ // for ensure +import cats.syntax.applicative.* // for pure +import cats.syntax.applicativeError.* // for raiseError etc +import cats.syntax.monadError.* // for ensure ``` ```scala mdoc @@ -165,7 +164,7 @@ always represent errors as `Throwables`: ```scala mdoc:silent import scala.util.Try -import cats.instances.try_._ // for MonadError +import cats.instances.try_.* // for MonadError val exn: Throwable = new RuntimeException("It's all gone wrong") @@ -180,14 +179,14 @@ exn.raiseError[Try, Int] Implement a method `validateAdult` with the following signature ```scala -def validateAdult[F[_]](age: Int)(implicit me: MonadError[F, Throwable]): F[Int] = +def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int] = ??? ``` When passed an `age` greater than or equal to 18 it should return that value as a success. Otherwise it should return a error represented as an `IllegalArgumentException`. ```scala mdoc:invisible -def validateAdult[F[_]](age: Int)(implicit me: MonadError[F, Throwable]): F[Int] = +def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int] = if(age >= 18) age.pure[F] else new IllegalArgumentException("Age must be greater than or equal to 18").raiseError[F, Int] ``` @@ -206,10 +205,10 @@ We can solve this using `pure` and `raiseError`. Note the use of type parameters ```scala mdoc:invisible:reset-object import cats.MonadError -import cats.implicits._ +import cats.implicits.* ``` ```scala mdoc:silent -def validateAdult[F[_]](age: Int)(implicit me: MonadError[F, Throwable]): F[Int] = +def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int] = if(age >= 18) age.pure[F] else new IllegalArgumentException("Age must be greater than or equal to 18").raiseError[F, Int] ``` diff --git a/src/pages/monads/reader.md b/src/pages/monads/reader.md index efdee2c5..0bbf2799 100644 --- a/src/pages/monads/reader.md +++ b/src/pages/monads/reader.md @@ -184,7 +184,7 @@ def checkPassword( Reader(db => db.passwords.get(username).contains(password)) ``` ```scala mdoc:silent -import cats.syntax.applicative._ // for pure +import cats.syntax.applicative.* // for pure def checkLogin( userId: Int, diff --git a/src/pages/monads/state.md b/src/pages/monads/state.md index b7d137d3..32616453 100644 --- a/src/pages/monads/state.md +++ b/src/pages/monads/state.md @@ -121,7 +121,7 @@ that only represent transformations on the state: ```scala mdoc:silent:reset-object import cats.data.State -import State._ +import State.* ``` ```scala mdoc @@ -234,13 +234,12 @@ type CalcState[A] = State[List[Int], A] ``` ```scala def evalOne(sym: String): CalcState[Int] = - sym match { + sym match case "+" => operator(_ + _) case "-" => operator(_ - _) case "*" => operator(_ * _) case "/" => operator(_ / _) case num => operand(num.toInt) - } ``` Let's look at `operand` first. @@ -275,13 +274,12 @@ def operator(func: (Int, Int) => Int): CalcState[Int] = ```scala mdoc:invisible def evalOne(sym: String): CalcState[Int] = - sym match { + sym match case "+" => operator(_ + _) case "-" => operator(_ - _) case "*" => operator(_ * _) case "/" => operator(_ / _) case num => operand(num.toInt) - } ```
@@ -342,16 +340,15 @@ def operator(func: (Int, Int) => Int): CalcState[Int] = sys.error("Fail!") } def evalOne(sym: String): CalcState[Int] = - sym match { + sym match case "+" => operator(_ + _) case "-" => operator(_ - _) case "*" => operator(_ * _) case "/" => operator(_ / _) case num => operand(num.toInt) - } ``` ```scala mdoc:silent -import cats.syntax.applicative._ // for pure +import cats.syntax.applicative.* // for pure def evalAll(input: List[String]): CalcState[Int] = input.foldLeft(0.pure[CalcState]) { (a, b) => diff --git a/src/pages/monads/writer.md b/src/pages/monads/writer.md index d17d0bdf..7fa3d005 100644 --- a/src/pages/monads/writer.md +++ b/src/pages/monads/writer.md @@ -34,7 +34,7 @@ We can create a `Writer` from values of each type as follows: ```scala mdoc:silent import cats.data.Writer -import cats.instances.vector._ // for Monoid +import cats.instances.vector.* // for Monoid ``` ```scala mdoc @@ -67,8 +67,8 @@ To do this we must have a `Monoid[W]` in scope so Cats knows how to produce an empty log: ```scala mdoc:silent -import cats.instances.vector._ // for Monoid -import cats.syntax.applicative._ // for pure +import cats.instances.vector.* // for Monoid +import cats.syntax.applicative.* // for pure type Logged[A] = Writer[Vector[String], A] ``` @@ -82,7 +82,7 @@ we can create a `Writer[Unit]` using the `tell` syntax from [`cats.syntax.writer`][cats.syntax.writer]: ```scala mdoc:silent -import cats.syntax.writer._ // for tell +import cats.syntax.writer.* // for tell ``` ```scala mdoc @@ -95,7 +95,7 @@ or we can use the `writer` syntax from [`cats.syntax.writer`][cats.syntax.writer]: ```scala mdoc:silent -import cats.syntax.writer._ // for writer +import cats.syntax.writer.* // for writer ``` ```scala mdoc @@ -214,9 +214,9 @@ This makes it difficult to see which messages come from which computation: ```scala -import scala.concurrent._ -import scala.concurrent.ExecutionContext.Implicits._ -import scala.concurrent.duration._ +import scala.concurrent.* +import scala.concurrent.ExecutionContext.Implicits.* +import scala.concurrent.duration.* Await.result(Future.sequence(Vector( Future(factorial(5)), @@ -255,8 +255,8 @@ so we can use it with `pure` syntax: ```scala mdoc:silent:reset-object import cats.data.Writer -import cats.instances.vector._ -import cats.syntax.applicative._ // for pure +import cats.instances.vector.* +import cats.syntax.applicative.* // for pure type Logged[A] = Writer[Vector[String], A] ``` @@ -268,7 +268,7 @@ type Logged[A] = Writer[Vector[String], A] We'll import the `tell` syntax as well: ```scala mdoc:silent -import cats.syntax.writer._ // for tell +import cats.syntax.writer.* // for tell ``` ```scala mdoc @@ -280,7 +280,7 @@ the `Semigroup` instance for `Vector`. We need this to `map` and `flatMap` over `Logged`: ```scala mdoc:silent -import cats.instances.vector._ // for Monoid +import cats.instances.vector.* // for Monoid ``` ```scala mdoc diff --git a/src/pages/monoids/cats.md b/src/pages/monoids/cats.md index 8c430d3e..40dec61f 100644 --- a/src/pages/monoids/cats.md +++ b/src/pages/monoids/cats.md @@ -45,12 +45,12 @@ are defined directly in the [`cats`][cats.package] package. the companion object has an `apply` method that returns the type class instance for a particular type. For example, if we want the monoid instance for `String`, -and we have the correct implicits in scope, +and we have the correct given instances in scope, we can write the following: ```scala mdoc:silent import cats.Monoid -import cats.instances.string._ // for Monoid +import cats.instances.string.* // for Monoid ``` ```scala mdoc @@ -85,7 +85,7 @@ we import from [`cats.instances.int`][cats.instances.int]: ```scala mdoc:silent import cats.Monoid -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc @@ -98,8 +98,8 @@ and [`cats.instances.option`][cats.instances.option]: ```scala mdoc:silent import cats.Monoid -import cats.instances.int._ // for Monoid -import cats.instances.option._ // for Monoid +import cats.instances.int.* // for Monoid +import cats.instances.option.* // for Monoid ``` ```scala mdoc @@ -116,8 +116,8 @@ As always, unless we have a good reason to import individual instances, we can just import everything. ```scala -import cats._ -import cats.implicits._ +import cats.* +import cats.implicits.* ``` ### Monoid Syntax {#sec:monoid-syntax} @@ -128,8 +128,8 @@ Because `combine` technically comes from `Semigroup`, we access the syntax by importing from [`cats.syntax.semigroup`][cats.syntax.semigroup]: ```scala mdoc:silent -import cats.instances.string._ // for Monoid -import cats.syntax.semigroup._ // for |+| +import cats.instances.string.* // for Monoid +import cats.syntax.semigroup.* // for |+| ``` ```scala mdoc @@ -137,7 +137,7 @@ val stringResult = "Hi " |+| "there" |+| Monoid[String].empty ``` ```scala mdoc:silent -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc @@ -163,8 +163,8 @@ although there's not a compelling use case for this yet: ```scala mdoc:silent:reset-object import cats.Monoid -import cats.instances.int._ // for Monoid -import cats.syntax.semigroup._ // for |+| +import cats.instances.int.* // for Monoid +import cats.syntax.semigroup.* // for |+| def add(items: List[Int]): Int = items.foldLeft(Monoid[Int].empty)(_ |+| _) @@ -185,9 +185,9 @@ We can write this as a generic method that accepts an implicit `Monoid` as a par ```scala mdoc:silent:reset-object import cats.Monoid -import cats.syntax.semigroup._ // for |+| +import cats.syntax.semigroup.* // for |+| -def add[A](items: List[A])(implicit monoid: Monoid[A]): A = +def add[A](items: List[A])(using monoid: Monoid[A]): A = items.foldLeft(monoid.empty)(_ |+| _) ``` @@ -195,8 +195,8 @@ We can optionally use Scala's *context bound* syntax to write the same code in a ```scala mdoc:invisible:reset-object import cats.Monoid -import cats.instances.int._ // for Monoid -import cats.syntax.semigroup._ // for |+| +import cats.instances.int.* // for Monoid +import cats.syntax.semigroup.* // for |+| ``` ```scala mdoc:silent def add[A: Monoid](items: List[A]): A = @@ -206,7 +206,7 @@ def add[A: Monoid](items: List[A]): A = We can use this code to add values of type `Int` and `Option[Int]` as requested: ```scala mdoc:silent -import cats.instances.int._ // for Monoid +import cats.instances.int.* // for Monoid ``` ```scala mdoc @@ -214,7 +214,7 @@ add(List(1, 2, 3)) ``` ```scala mdoc:silent -import cats.instances.option._ // for Monoid +import cats.instances.option.* // for Monoid ``` ```scala mdoc @@ -247,7 +247,7 @@ Make it so! Easy---we simply define a monoid instance for `Order`! ```scala mdoc:silent -implicit val monoid: Monoid[Order] = new Monoid[Order] { +given monoid: Monoid[Order] with def combine(o1: Order, o2: Order) = Order( o1.totalCost + o2.totalCost, @@ -255,6 +255,5 @@ implicit val monoid: Monoid[Order] = new Monoid[Order] { ) def empty = Order(0, 0) -} ``` diff --git a/src/pages/monoids/index.md b/src/pages/monoids/index.md index 46d2f53b..e95f3dd6 100644 --- a/src/pages/monoids/index.md +++ b/src/pages/monoids/index.md @@ -100,10 +100,9 @@ This definition translates nicely into Scala code. Here is a simplified version of the definition from Cats: ```scala mdoc:silent -trait Monoid[A] { +trait Monoid[A]: def combine(x: A, y: A): A def empty: A -} ``` In addition to providing the `combine` and `empty` operations, @@ -114,13 +113,13 @@ For all values `x`, `y`, and `z`, in `A`, ```scala mdoc:silent def associativeLaw[A](x: A, y: A, z: A) - (implicit m: Monoid[A]): Boolean = { + (using m: Monoid[A]): Boolean = { m.combine(x, m.combine(y, z)) == m.combine(m.combine(x, y), z) } def identityLaw[A](x: A) - (implicit m: Monoid[A]): Boolean = { + (using m: Monoid[A]): Boolean = { (m.combine(x, m.empty) == x) && (m.combine(m.empty, x) == x) } @@ -161,13 +160,11 @@ A more accurate (though still simplified) definition of Cats' [`Monoid`][cats.Monoid] is: ```scala mdoc:silent:reset-object -trait Semigroup[A] { +trait Semigroup[A]: def combine(x: A, y: A): A -} -trait Monoid[A] extends Semigroup[A] { +trait Monoid[A] extends Semigroup[A]: def empty: A -} ``` We'll see this kind of inheritance often when discussing type classes. @@ -185,18 +182,15 @@ and convince yourself that the monoid laws hold. Use the following definitions as a starting point: ```scala mdoc:reset:silent -trait Semigroup[A] { +trait Semigroup[A]: def combine(x: A, y: A): A -} -trait Monoid[A] extends Semigroup[A] { +trait Monoid[A] extends Semigroup[A]: def empty: A -} -object Monoid { - def apply[A](implicit monoid: Monoid[A]) = +object Monoid: + def apply[A](using monoid: Monoid[A]) = monoid -} ```
@@ -204,46 +198,38 @@ There are four monoids for `Boolean`! First, we have *and* with operator `&&` and identity `true`: ```scala mdoc:silent -implicit val booleanAndMonoid: Monoid[Boolean] = - new Monoid[Boolean] { - def combine(a: Boolean, b: Boolean) = a && b - def empty = true - } +given booleanAndMonoid: Monoid[Boolean] with + def combine(a: Boolean, b: Boolean) = a && b + def empty = true ``` Second, we have *or* with operator `||` and identity `false`: ```scala mdoc:silent -implicit val booleanOrMonoid: Monoid[Boolean] = - new Monoid[Boolean] { - def combine(a: Boolean, b: Boolean) = a || b - def empty = false - } +given booleanOrMonoid: Monoid[Boolean] with + def combine(a: Boolean, b: Boolean) = a || b + def empty = false ``` Third, we have *exclusive or* with identity `false`: ```scala mdoc:silent -implicit val booleanEitherMonoid: Monoid[Boolean] = - new Monoid[Boolean] { - def combine(a: Boolean, b: Boolean) = - (a && !b) || (!a && b) +given booleanEitherMonoid: Monoid[Boolean] with + def combine(a: Boolean, b: Boolean) = + (a && !b) || (!a && b) - def empty = false - } + def empty = false ``` Finally, we have *exclusive nor* (the negation of exclusive or) with identity `true`: ```scala mdoc:silent -implicit val booleanXnorMonoid: Monoid[Boolean] = - new Monoid[Boolean] { - def combine(a: Boolean, b: Boolean) = - (!a || b) && (a || !b) +given booleanXnorMonoid: Monoid[Boolean] with + def combine(a: Boolean, b: Boolean) = + (!a || b) && (a || !b) - def empty = true - } + def empty = true ``` Showing that the identity law holds in each case is straightforward. @@ -259,11 +245,9 @@ What monoids and semigroups are there for sets? *Set union* forms a monoid along with the empty set: ```scala mdoc:silent -implicit def setUnionMonoid[A]: Monoid[Set[A]] = - new Monoid[Set[A]] { - def combine(a: Set[A], b: Set[A]) = a union b - def empty = Set.empty[A] - } +given setUnionMonoid[A]: Monoid[Set[A]] with + def combine(a: Set[A], b: Set[A]) = a union b + def empty = Set.empty[A] ``` We need to define `setUnionMonoid` as a method @@ -285,11 +269,9 @@ Set intersection forms a semigroup, but doesn't form a monoid because it has no identity element: ```scala mdoc:silent -implicit def setIntersectionSemigroup[A]: Semigroup[Set[A]] = - new Semigroup[Set[A]] { - def combine(a: Set[A], b: Set[A]) = - a intersect b - } +given setIntersectionSemigroup[A]: Semigroup[Set[A]] with + def combine(a: Set[A], b: Set[A]) = + a intersect b ``` Set complement and set difference are not associative, @@ -301,11 +283,9 @@ does also form a monoid with the empty set: import cats.Monoid ``` ```scala mdoc:silent -implicit def symDiffMonoid[A]: Monoid[Set[A]] = - new Monoid[Set[A]] { - def combine(a: Set[A], b: Set[A]): Set[A] = - (a diff b) union (b diff a) - def empty: Set[A] = Set.empty - } +given symDiffMonoid[A]: Monoid[Set[A]] with + def combine(a: Set[A], b: Set[A]): Set[A] = + (a diff b) union (b diff a) + def empty: Set[A] = Set.empty ```
diff --git a/src/pages/monoids/summary.md b/src/pages/monoids/summary.md index bee25e28..233a3616 100644 --- a/src/pages/monoids/summary.md +++ b/src/pages/monoids/summary.md @@ -12,8 +12,8 @@ and the semigroup syntax to give us the `|+|` operator: ```scala mdoc:silent import cats.Monoid -import cats.instances.string._ // for Monoid -import cats.syntax.semigroup._ // for |+| +import cats.instances.string.* // for Monoid +import cats.syntax.semigroup.* // for |+| ``` ```scala mdoc @@ -24,8 +24,8 @@ With the correct instances in scope, we can set about adding anything we want: ```scala mdoc:silent -import cats.instances.int._ // for Monoid -import cats.instances.option._ // for Monoid +import cats.instances.int.* // for Monoid +import cats.instances.option.* // for Monoid ``` ```scala mdoc @@ -33,7 +33,7 @@ Option(1) |+| Option(2) ``` ```scala mdoc:silent -import cats.instances.map._ // for Monoid +import cats.instances.map.* // for Monoid val map1 = Map("a" -> 1, "b" -> 2) val map2 = Map("b" -> 3, "d" -> 4) @@ -44,7 +44,7 @@ map1 |+| map2 ``` ```scala mdoc:silent -import cats.instances.tuple._ // for Monoid +import cats.instances.tuple.* // for Monoid val tuple1 = ("hello", 123) @@ -60,7 +60,7 @@ for which we have an instance of `Monoid`: ```scala mdoc:silent def addAll[A](values: List[A]) - (implicit monoid: Monoid[A]): A = + (using monoid: Monoid[A]): A = values.foldRight(monoid.empty)(_ |+| _) ``` diff --git a/src/pages/preface/conventions.md b/src/pages/preface/conventions.md index f98ed4b1..9960e586 100644 --- a/src/pages/preface/conventions.md +++ b/src/pages/preface/conventions.md @@ -25,9 +25,8 @@ Source code blocks are written as follows. Syntax is highlighted appropriately where applicable: ```scala mdoc:silent -object MyApp extends App { +object MyApp extends App: println("Hello world!") // Print a fine message to the user! -} ``` Most code passes through [mdoc][link-mdoc] to ensure it compiles. diff --git a/src/pages/preface/versions.md b/src/pages/preface/versions.md index 9da7f8e5..9aefbc41 100644 --- a/src/pages/preface/versions.md +++ b/src/pages/preface/versions.md @@ -11,7 +11,8 @@ libraryDependencies += "org.typelevel" %% "cats-core" % "@CATS_VERSION@" scalacOptions ++= Seq( - "-Xfatal-warnings" + "-explain", + "-Werror" ) ``` @@ -32,19 +33,3 @@ with Cats as a dependency. See the generated `README.md` for instructions on how to run the sample code and/or start an interactive Scala console. - -The `cats-seed` template is very minimal. -If you'd prefer a more batteries-included starting point, -check out Typelevel's `sbt-catalysts` template: - -```bash -$ sbt new typelevel/sbt-catalysts.g8 -``` - -This will generate a project with a suite -of library dependencies and compiler plugins, -together with templates for unit tests -and documentation. -See the project pages for [catalysts][link-catalysts] -and [sbt-catalysts][link-sbt-catalysts] -for more information. diff --git a/src/pages/type-classes/anatomy.md b/src/pages/type-classes/anatomy.md index f61cb34a..1e019730 100644 --- a/src/pages/type-classes/anatomy.md +++ b/src/pages/type-classes/anatomy.md @@ -5,14 +5,14 @@ the *type class* itself, *instances* for particular types, and the methods that *use* type classes. -Type classes in Scala are implemented using *implicit values* and *parameters*, -and optionally using *implicit classes*. +Type classes in Scala are implemented using *traits*, *given instances* and *using clauses*, +and optionally using *extension methods*. Scala language constructs correspond to the components of type classes as follows: - traits: type classes; -- implicit values: type class instances; -- implicit parameters: type class use; and -- implicit classes: optional utilities that make type classes easier to use. +- given instances: type class instances; +- using clauses: type class use; and +- extension methods: optional utilities that make type classes easier to use. Let's see how this works in detail. @@ -27,16 +27,15 @@ as follows: ```scala mdoc:silent:reset-object // Define a very simple JSON AST -sealed trait Json -final case class JsObject(get: Map[String, Json]) extends Json -final case class JsString(get: String) extends Json -final case class JsNumber(get: Double) extends Json -final case object JsNull extends Json +enum Json: + case JsObject(get: Map[String, Json]) + case JsString(get: String) + case JsNumber(get: Double) + case JsNull // The "serialize to JSON" behaviour is encoded in this trait -trait JsonWriter[A] { +trait JsonWriter[A]: def write(value: A): Json -} ``` `JsonWriter` is our type class in this example, @@ -53,40 +52,33 @@ and types from our domain model. In Scala we define instances by creating concrete implementations of the type class -and tagging them with the `implicit` keyword: +and tagging them with the `given` keyword: ```scala mdoc:silent final case class Person(name: String, email: String) -object JsonWriterInstances { - implicit val stringWriter: JsonWriter[String] = - new JsonWriter[String] { - def write(value: String): Json = - JsString(value) - } - - implicit val personWriter: JsonWriter[Person] = - new JsonWriter[Person] { - def write(value: Person): Json = - JsObject(Map( - "name" -> JsString(value.name), - "email" -> JsString(value.email) - )) - } - - // etc... -} -``` +given stringWriter: JsonWriter[String] with + def write(value: String): Json = + Json.JsString(value) + +given personWriter: JsonWriter[Person] with + def write(value: Person): Json = + Json.JsObject(Map( + "name" -> Json.JsString(value.name), + "email" -> Json.JsString(value.email) + )) -These are known as implicit values. +// etc... +``` +These are known as given instances. ### Type Class Use A type class *use* is any functionality that requires a type class instance to work. In Scala this means any method -that accepts instances of the type class as implicit parameters. +that accepts instances of the type class as using clauses. Cats provides utilities that make type classes easier to use, and you will sometimes seem these patterns in other libraries. @@ -98,30 +90,25 @@ The simplest way of creating an interface that uses a type class is to place methods in a singleton object: ```scala mdoc:silent -object Json { - def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = +object Json: + def toJson[A](value: A)(using w: JsonWriter[A]): Json = w.write(value) -} ``` To use this object, we import any type class instances we care about and call the relevant method: -```scala mdoc:silent -import JsonWriterInstances._ -``` - ```scala mdoc Json.toJson(Person("Dave", "dave@example.com")) ``` The compiler spots that we've called the `toJson` method -without providing the implicit parameters. +without providing the using clauses. It tries to fix this by searching for type class instances of the relevant types and inserting them at the call site: ```scala mdoc:silent -Json.toJson(Person("Dave", "dave@example.com"))(personWriter) +Json.toJson(Person("Dave", "dave@example.com"))(using personWriter) ``` **Interface Syntax** @@ -135,55 +122,44 @@ referred to as "type enrichment" or "pimping". These are older terms that we don't use anymore. ```scala mdoc:silent -object JsonSyntax { - implicit class JsonWriterOps[A](value: A) { - def toJson(implicit w: JsonWriter[A]): Json = - w.write(value) - } -} +extension [A](value: A) + def toJson(using w: JsonWriter[A]): Json = + w.write(value) ``` We use interface syntax by importing it alongside the instances for the types we need: -```scala mdoc:silent -import JsonWriterInstances._ -import JsonSyntax._ -``` - ```scala mdoc Person("Dave", "dave@example.com").toJson ``` Again, the compiler searches for candidates -for the implicit parameters and fills them in for us: +for the using clauses and fills them in for us: ```scala mdoc:silent -Person("Dave", "dave@example.com").toJson(personWriter) +Person("Dave", "dave@example.com").toJson(using personWriter) ``` -**The *implicitly* Method** +**The *summon* Method** The Scala standard library provides -a generic type class interface called `implicitly`. +a generic type class interface called `summon`. Its definition is very simple: ```scala -def implicitly[A](implicit value: A): A = - value +def summon[A](using value: A): A = value ``` -We can use `implicitly` to summon any value from implicit scope. -We provide the type we want and `implicitly` does the rest: +We can use `summon` to summon any value from the contextual abstractions scope. +We provide the type we want and `summon` does the rest: ```scala mdoc -import JsonWriterInstances._ - -implicitly[JsonWriter[String]] +summon[JsonWriter[String]] ``` Most type classes in Cats provide other means to summon instances. -However, `implicitly` is a good fallback for debugging purposes. -We can insert a call to `implicitly` within the general flow of our code +However, `summon` is a good fallback for debugging purposes. +We can insert a call to `summon` within the general flow of our code to ensure the compiler can find an instance of a type class -and ensure that there are no ambiguous implicit errors. +and ensure that there are no ambiguous given instances errors. diff --git a/src/pages/type-classes/cats.md b/src/pages/type-classes/cats.md index 6e53501d..5f396e12 100644 --- a/src/pages/type-classes/cats.md +++ b/src/pages/type-classes/cats.md @@ -17,9 +17,8 @@ Here's an abbreviated definition: ```scala package cats -trait Show[A] { +trait Show[A]: def show(value: A): String -} ``` ### Importing Type Classes @@ -35,13 +34,9 @@ The companion object of every Cats type class has an `apply` method that locates an instance for any type we specify: ```scala mdoc -val showInt = Show.apply[Int] +given showInt: Show[Int] = Show.apply[Int] ``` -Oops---that didn't work! -The `apply` method uses *implicits* to look up individual instances, -so we'll have to bring some instances into scope. - ### Importing Default Instances {#sec:importing-default-instances} The [`cats.instances`][cats.instances] package @@ -63,8 +58,8 @@ Let's import the instances of `Show` for `Int` and `String`: ```scala mdoc:reset:silent import cats.Show -import cats.instances.int._ // for Show -import cats.instances.string._ // for Show +import cats.instances.int.* // for Show +import cats.instances.string.* // for Show val showInt: Show[Int] = Show.apply[Int] val showString: Show[String] = Show.apply[String] @@ -89,7 +84,7 @@ This adds an extension method called `show` to any type for which we have an instance of `Show` in scope: ```scala mdoc:silent -import cats.syntax.show._ // for show +import cats.syntax.show.* // for show ``` ```scala mdoc @@ -108,9 +103,9 @@ exactly which instances and syntax you need in each example. However, this doesn't add value in production code. It is simpler and faster to use the following imports: -- `import cats._` imports all of Cats' type classes in one go; +- `import cats.*` imports all of Cats' type classes in one go; -- `import cats.implicits._` imports +- `import cats.implicits.*` imports all of the standard type class instances *and* all of the syntax in one go. @@ -123,11 +118,9 @@ simply by implementing the trait for a given type: ```scala mdoc:silent import java.util.Date -implicit val dateShow: Show[Date] = - new Show[Date] { - def show(date: Date): String = - s"${date.getTime}ms since the epoch." - } +given dateShow: Show[Date] with + def show(date: Date): String = + s"${date.getTime}ms since the epoch." ``` ```scala mdoc new Date().show @@ -139,7 +132,7 @@ There are two construction methods on the companion object of `Show` that we can use to define instances for our own types: ```scala -object Show { +object Show: // Convert a function to a `Show` instance: def show[A](f: A => String): Show[A] = ??? @@ -147,7 +140,6 @@ object Show { // Create a `Show` instance from a `toString` method: def fromToString[A]: Show[A] = ??? -} ``` These allow us to quickly construct instances @@ -158,7 +150,7 @@ import cats.Show import java.util.Date ``` ```scala mdoc:silent -implicit val dateShow: Show[Date] = +given dateShow: Show[Date] = Show.show(date => s"${date.getTime}ms since the epoch.") ``` @@ -181,9 +173,9 @@ and the interface syntax: ```scala mdoc:reset-object:silent import cats.Show -import cats.instances.int._ // for Show -import cats.instances.string._ // for Show -import cats.syntax.show._ // for show +import cats.instances.int.* // for Show +import cats.instances.string.* // for Show +import cats.syntax.show.* // for show ``` Our definition of `Cat` remains the same: @@ -196,7 +188,7 @@ In the companion object we replace our `Printable` with an instance of `Show` using one of the definition helpers discussed above: ```scala mdoc:silent -implicit val catShow: Show[Cat] = Show.show[Cat] { cat => +given catShow: Show[Cat] = Show.show[Cat] { cat => val name = cat.name.show val age = cat.age.show val color = cat.color.show diff --git a/src/pages/type-classes/equal.md b/src/pages/type-classes/equal.md index 14340a9d..131a270b 100644 --- a/src/pages/type-classes/equal.md +++ b/src/pages/type-classes/equal.md @@ -34,10 +34,9 @@ between instances of any given type: ```scala package cats -trait Eq[A] { +trait Eq[A]: def eqv(a: A, b: A): Boolean // other concrete methods based on eqv... -} ``` The interface syntax, defined in [`cats.syntax.eq`][cats.syntax.eq], @@ -58,7 +57,7 @@ import cats.Eq Now let's grab an instance for `Int`: ```scala mdoc:silent -import cats.instances.int._ // for Eq +import cats.instances.int.* // for Eq val eqInt = Eq[Int] ``` @@ -82,7 +81,7 @@ We can also import the interface syntax in [`cats.syntax.eq`][cats.syntax.eq] to use the `===` and `=!=` methods: ```scala mdoc:silent -import cats.syntax.eq._ // for === and =!= +import cats.syntax.eq.* // for === and =!= ``` ```scala mdoc @@ -103,8 +102,8 @@ To compare values of type `Option[Int]` we need to import instances of `Eq` for `Option` as well as `Int`: ```scala mdoc:silent -import cats.instances.int._ // for Eq -import cats.instances.option._ // for Eq +import cats.instances.int.* // for Eq +import cats.instances.option.* // for Eq ``` Now we can try some comparisons: @@ -132,7 +131,7 @@ Option(1) === Option.empty[Int] or using special syntax from [`cats.syntax.option`][cats.syntax.option]: ```scala mdoc:silent -import cats.syntax.option._ // for some and none +import cats.syntax.option.* // for some and none ``` ```scala mdoc @@ -147,11 +146,11 @@ which accepts a function of type `(A, A) => Boolean` and returns an `Eq[A]`: ```scala mdoc:silent import java.util.Date -import cats.instances.long._ // for Eq +import cats.instances.long.* // for Eq ``` ```scala mdoc:silent -implicit val dateEq: Eq[Date] = +given dateEq: Eq[Date] = Eq.instance[Date] { (date1, date2) => date1.getTime === date2.getTime } @@ -193,7 +192,7 @@ We'll bring instances of `Eq` into scope as we need them below: ```scala mdoc:silent:reset-object import cats.Eq -import cats.syntax.eq._ // for === +import cats.syntax.eq.* // for === ``` Our `Cat` class is the same as ever: @@ -206,10 +205,10 @@ We bring the `Eq` instances for `Int` and `String` into scope for the implementation of `Eq[Cat]`: ```scala mdoc:silent -import cats.instances.int._ // for Eq -import cats.instances.string._ // for Eq +import cats.instances.int.* // for Eq +import cats.instances.string.* // for Eq -implicit val catEqual: Eq[Cat] = +given catEqual: Eq[Cat] = Eq.instance[Cat] { (cat1, cat2) => (cat1.name === cat2.name ) && (cat1.age === cat2.age ) && @@ -228,7 +227,7 @@ cat1 =!= cat2 ``` ```scala mdoc:silent -import cats.instances.option._ // for Eq +import cats.instances.option.* // for Eq ``` ```scala mdoc diff --git a/src/pages/type-classes/implicits.md b/src/pages/type-classes/implicits.md index 1f179e0f..319beffc 100644 --- a/src/pages/type-classes/implicits.md +++ b/src/pages/type-classes/implicits.md @@ -1,67 +1,42 @@ -## Working with Implicits +## Working with Contextual Abstractions ```scala mdoc:invisible // Forward definitions -sealed trait Json -case class JsObject(get: Map[String, Json]) extends Json -case class JsString(get: String) extends Json -case class JsNumber(get: Double) extends Json -case object JsNull extends Json +enum Json: + case JsObject(get: Map[String, Json]) + case JsString(get: String) + case JsNumber(get: Double) + case JsNull -trait JsonWriter[A] { +trait JsonWriter[A]: def write(value: A): Json -} case class Person(name: String, email: String) -object JsonWriterInstances { - implicit val stringWriter: JsonWriter[String] = - new JsonWriter[String] { - def write(value: String): Json = - JsString(value) - } - - implicit val personWriter: JsonWriter[Person] = - new JsonWriter[Person] { - def write(value: Person): Json = - JsObject(Map( - "name" -> JsString(value.name), - "email" -> JsString(value.email) - )) - } - - // etc... -} - -import JsonWriterInstances._ - -object Json { - def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = +given stringWriter: JsonWriter[String] with + def write(value: String): Json = + Json.JsString(value) + +given personWriter: JsonWriter[Person] with + def write(value: Person): Json = + Json.JsObject(Map( + "name" -> Json.JsString(value.name), + "email" -> Json.JsString(value.email) + )) + +// etc... + +object Json: + def toJson[A](value: A)(using w: JsonWriter[A]): Json = w.write(value) -} ``` Working with type classes in Scala means -working with implicit values and implicit parameters. +working with given instances and using clauses. There are a few rules we need to know to do this effectively. - -### Packaging Implicits - -In a curious quirk of the language, -any definitions marked `implicit` in Scala must be placed -inside an object or trait rather than at the top level. -In the example above we packaged our type class instances -in an object called `JsonWriterInstances`. -We could equally have placed them -in a companion object to `JsonWriter`. -Placing instances in a companion object -to the type class has special significance in Scala -because it plays into something called *implicit scope*. - - -### Implicit Scope +### Contextual Abstractions Scope As we saw above, the compiler searches for candidate type class instances by type. @@ -74,10 +49,10 @@ Json.toJson("A string!") ``` The places where the compiler searches for candidate instances -is known as the *implicit scope*. -The implicit scope applies at the call site; -that is the point where we call a method with an implicit parameter. -The implicit scope which roughly consists of: +is known as the *contextual abstractions scope*. +The contextual abstractions scope applies at the call site; +that is the point where we call a method with a using clause. +The contextual abstractions scope which roughly consists of: - local or inherited definitions; @@ -87,109 +62,96 @@ The implicit scope which roughly consists of: of the type class or the parameter type (in this case `JsonWriter` or `String`). -Definitions are only included in implicit scope -if they are tagged with the `implicit` keyword. Furthermore, if the compiler sees multiple candidate definitions, -it fails with an *ambiguous implicit values* error: +it fails with an *ambiguous given instances* error: ```scala mdoc:invisible:reset-object -sealed trait Json -case class JsObject(get: Map[String, Json]) extends Json -case class JsString(get: String) extends Json -case class JsNumber(get: Double) extends Json -case object JsNull extends Json +enum Json: + case JsObject(get: Map[String, Json]) + case JsString(get: String) + case JsNumber(get: Double) + case JsNull -trait JsonWriter[A] { +trait JsonWriter[A]: def write(value: A): Json -} - -object JsonWriterInstances { - implicit val stringWriter: JsonWriter[String] = - new JsonWriter[String] { - def write(value: String): Json = - JsString(value) - } -} - -object Json { - def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = + +given stringWriter: JsonWriter[String] with + def write(value: String): Json = + Json.JsString(value) + +object Json: + def toJson[A](value: A)(using w: JsonWriter[A]): Json = w.write(value) -} ``` ```scala mdoc:fail -implicit val writer1: JsonWriter[String] = - JsonWriterInstances.stringWriter - -implicit val writer2: JsonWriter[String] = - JsonWriterInstances.stringWriter +given secondStringWriter: JsonWriter[String] = stringWriter Json.toJson("A string") ``` -The precise rules of implicit resolution are more complex than this, +The precise rules of given instance resolution are more complex than this, but the complexity is largely irrelevant for day-to-day use[^implicit-search]. -For our purposes, we can package type class instances in roughly four ways: +For our purposes, we can package type class instances in roughly five ways: -1. by placing them in an object such as `JsonWriterInstances`; -2. by placing them in a trait; -3. by placing them in the companion object of the type class; -4. by placing them in the companion object of the parameter type. +1. by placing them as top level definitions in a package. +2. by placing them in an object such as `JsonWriterInstances`; +3. by placing them in a trait; +4. by placing them in the companion object of the type class; +5. by placing them in the companion object of the parameter type. -With option 1 we bring instances into scope by `importing` them. -With option 2 we bring them into scope with inheritance. -With options 3 and 4 instances are *always* in implicit scope, +With option 1 and 2 we bring given instances into scope by `importing` them explicitly. +With option 3 we bring them into scope with inheritance. +With options 4 and 5 instances are *always* in the contextual abstractions scope, regardless of where we try to use them. -It is conventional to put type class instances in a companion object (option 3 and 4 above) +It is conventional to put type class instances in a companion object (option 4 and 5 above) if there is only one sensible implementation, or at least one implementation that is widely accepted as the default. This makes type class instances easier to use -as no import is required to bring them into the implicit scope. +as no import is required to bring them into the contextual abstractions scope. [^implicit-search]: If you're interested in the finer rules of implicit resolution in Scala, start by taking a look at [this Stack Overflow post on implicit scope][link-so-implicit-scope] and [this blog post on implicit priority][link-implicit-priority]. -### Recursive Implicit Resolution {#sec:type-classes:recursive-implicits} +### Recursive Given Instance Resolution {#sec:type-classes:recursive-implicits} -The power of type classes and implicits lies in -the compiler's ability to *combine* implicit definitions +The power of type classes with given instances and using clauses lies in +the compiler's ability to *combine* given instances definitions when searching for candidate instances. This is sometimes known as *type class composition*. Earlier we insinuated that all type class instances -are `implicit vals`. This was a simplification. +are `given`. This was a simplification. We can actually define instances in two ways: 1. by defining concrete instances as - `implicit vals` of the required type[^implicit-objects]; + `given` of the required type; -2. by defining `implicit` methods to +2. by defining `given` methods to construct instances from other type class instances. -[^implicit-objects]: We can also use an `implicit object`, which provides the same thing as an `implicit val`. - Why would we construct instances from other instances? As a motivational example, consider defining a `JsonWriter` for `Option`. We would need a `JsonWriter[Option[A]]` for every `A` we care about in our application. We could try to brute force the problem by creating -a library of `implicit vals`: +a library of `given`s: ```scala -implicit val optionIntWriter: JsonWriter[Option[Int]] = +given optionIntWriter: JsonWriter[Option[Int]] with ??? -implicit val optionPersonWriter: JsonWriter[Option[Person]] = +given optionPersonWriter: JsonWriter[Option[Person]] with ??? // and so on... ``` However, this approach clearly doesn't scale. -We end up requiring two `implicit vals` +We end up requiring two `given` instances for every type `A` in our application: one for `A` and one for `Option[A]`. @@ -201,66 +163,61 @@ into a common constructor based on the instance for `A`: - if the option is `None`, return `JsNull`. -Here is the same code written out as an `implicit def`: +Here is the same code written out as a `given` with a `using` clause: ```scala mdoc:silent -implicit def optionWriter[A] - (implicit writer: JsonWriter[A]): JsonWriter[Option[A]] = - new JsonWriter[Option[A]] { - def write(option: Option[A]): Json = - option match { - case Some(aValue) => writer.write(aValue) - case None => JsNull - } - } +given optionWriter[A](using writer: JsonWriter[A]): JsonWriter[Option[A]] with + def write(option: Option[A]): Json = + option match + case Some(aValue) => writer.write(aValue) + case None => Json.JsNull ``` This method *constructs* a `JsonWriter` for `Option[A]` by -relying on an implicit parameter to +relying on a using clause to fill in the `A`-specific functionality. When the compiler sees an expression like this: -```scala mdoc:invisible -import JsonWriterInstances._ -``` ```scala mdoc:silent Json.toJson(Option("A string")) ``` -it searches for an implicit `JsonWriter[Option[String]]`. -It finds the implicit method for `JsonWriter[Option[A]]`: +it searches for a given instance `JsonWriter[Option[String]]`. +It finds the given instance for `JsonWriter[Option[A]]`: ```scala mdoc:silent -Json.toJson(Option("A string"))(optionWriter[String]) +Json.toJson(Option("A string"))(using optionWriter[String]) ``` and recursively searches for a `JsonWriter[String]` -to use as the parameter to `optionWriter`: +for the using clause to `optionWriter`: ```scala mdoc:silent -Json.toJson(Option("A string"))(optionWriter(stringWriter)) +Json.toJson(Option("A string"))(using optionWriter(using stringWriter)) ``` -In this way, implicit resolution becomes +In this way, given instance resolution becomes a search through the space of possible combinations -of implicit definitions, to find +of given definitions, to find a combination that creates a type class instance of the correct overall type.
-*Implicit Conversions* +*Contextual Implicit Conversions* + +// TODO: https://docs.scala-lang.org/scala3/reference/contextual/conversions.html When you create a type class instance constructor -using an `implicit def`, +using an `given`, be sure to mark the parameters to the method -as `implicit` parameters. +as `using` parameters. Without this keyword, the compiler won't be able to -fill in the parameters during implicit resolution. +fill in the parameters during given instance resolution. -`implicit` methods with non-`implicit` parameters +`given` methods with non-`using` parameters form a different Scala pattern called an *implicit conversion*. This is also different from the previous section on `Interface Syntax`, -because in that case the `JsonWriter` is an implicit class with extension methods. +because in that case the `JsonWriter` has extension methods. Implicit conversion is an older programming pattern that is frowned upon in modern Scala code. Fortunately, the compiler will warn you when you do this. @@ -269,19 +226,12 @@ by importing `scala.language.implicitConversions` in your file: ```scala mdoc:invisible:reset type Json = Nothing -trait JsonWriter[A] { +trait JsonWriter[A]: def write(value: A): Json -} ``` -```scala -implicit def optionWriter[A] - (writer: JsonWriter[A]): JsonWriter[Option[A]] = +```scala modc:warn +given optionWriter[A](writer: JsonWriter[A]): JsonWriter[Option[A]] = ??? -// warning: implicit conversion method foo should be enabled -// by making the implicit value scala.language.implicitConversions visible. -// This can be achieved by adding the import clause 'import scala.language.implicitConversions' -// or by setting the compiler option -language:implicitConversions. -// See the Scaladoc for value scala.language.implicitConversions for a discussion -// why the feature should be explicitly enabled. +// TODO: Fix formatting ```
diff --git a/src/pages/type-classes/instance-selection.md b/src/pages/type-classes/instance-selection.md index 00f953c3..b4e0e592 100644 --- a/src/pages/type-classes/instance-selection.md +++ b/src/pages/type-classes/instance-selection.md @@ -23,8 +23,8 @@ that control instance selection: When we define type classes we can add variance annotations to the type parameter to affect the variance of the type class -and the compiler's ability to select instances -during implicit resolution. +and the compiler's ability to select given instances +during resolution. To recap Essential Scala, variance relates to subtypes. @@ -59,17 +59,17 @@ anywhere we expect a `List[Shape]` because `Circle` is a subtype of `Shape`: ```scala mdoc:silent -sealed trait Shape -case class Circle(radius: Double) extends Shape +enum Shape: + case Circle(radius: Double) ``` ```scala -val circles: List[Circle] = ??? +val circles: List[Shape.Circle] = ??? val shapes: List[Shape] = circles ``` ```scala mdoc:invisible -val circles: List[Circle] = null +val circles: List[Shape.Circle] = null val shapes: List[Shape] = circles ``` @@ -98,32 +98,31 @@ trait Json ``` ```scala mdoc -trait JsonWriter[-A] { +trait JsonWriter[-A]: def write(value: A): Json -} ``` Let's unpack this a bit further. Remember that variance is all about the ability to substitute one value for another. Consider a scenario where we have two values, -one of type `Shape` and one of type `Circle`, -and two `JsonWriters`, one for `Shape` and one for `Circle`: +one of type `Shape` and one of type `Shape.Circle`, +and two `JsonWriters`, one for `Shape` and one for `Shape.Circle`: ```scala val shape: Shape = ??? -val circle: Circle = ??? +val circle: Shape.Circle = ??? val shapeWriter: JsonWriter[Shape] = ??? -val circleWriter: JsonWriter[Circle] = ??? +val circleWriter: JsonWriter[Shape.Circle] = ??? ``` ```scala mdoc:invisible val shape: Shape = null -val circle: Circle = null +val circle: Shape.Circle = null val shapeWriter: JsonWriter[Shape] = null -val circleWriter: JsonWriter[Circle] = null +val circleWriter: JsonWriter[Shape.Circle] = null ``` ```scala mdoc:silent @@ -133,16 +132,16 @@ def format[A](value: A, writer: JsonWriter[A]): Json = Now ask yourself the question: "Which combinations of value and writer can I pass to `format`?" -We can `write` a `Circle` with either writer +We can `write` a `Shape.Circle` with either writer because all `Circles` are `Shapes`. Conversely, we can't write a `Shape` with `circleWriter` because not all `Shapes` are `Circles`. This relationship is what we formally model using contravariance. -`JsonWriter[Shape]` is a subtype of `JsonWriter[Circle]` -because `Circle` is a subtype of `Shape`. +`JsonWriter[Shape]` is a subtype of `JsonWriter[Shape.Circle]` +because `Shape.Circle` is a subtype of `Shape`. This means we can use `shapeWriter` -anywhere we expect to see a `JsonWriter[Circle]`. +anywhere we expect to see a `JsonWriter[Shape.Circle]`. **Invariance** @@ -160,7 +159,7 @@ are never subtypes of one another, no matter what the relationship between `A` and `B`. This is the default semantics for Scala type constructors. -When the compiler searches for an implicit +When the compiler searches for a given instance it looks for one matching the type *or subtype*. Thus we can use variance annotations to control type class instance selection to some extent. @@ -169,9 +168,8 @@ There are two issues that tend to arise. Let's imagine we have an algebraic data type like: ```scala -sealed trait A -final case object B extends A -final case object C extends A +enum A: + case B, C ``` The issues are: @@ -179,13 +177,13 @@ The issues are: 1. Will an instance defined on a supertype be selected if one is available? For example, can we define an instance for `A` - and have it work for values of type `B` and `C`? + and have it work for values of type `A.B` and `A.C`? 2. Will an instance for a subtype be selected in preference to that of a supertype. - For instance, if we define an instance for `A` and `B`, - and we have a value of type `B`, - will the instance for `B` be selected in preference to `A`? + For instance, if we define an instance for `A` and `A.B`, + and we have a value of type `A.B`, + will the instance for `A.B` be selected in preference to `A`? It turns out we can't have both at once. The three choices give us behaviour as follows: diff --git a/src/pages/type-classes/printable.md b/src/pages/type-classes/printable.md index 8153fb17..1e86b214 100644 --- a/src/pages/type-classes/printable.md +++ b/src/pages/type-classes/printable.md @@ -12,8 +12,7 @@ Let's define a `Printable` type class to work around these problems: 1. Define a type class `Printable[A]` containing a single method `format`. `format` should accept a value of type `A` and return a `String`. - 2. Create an object `PrintableInstances` - containing instances of `Printable` for `String` and `Int`. + 2. Create instances of `Printable` for `String` and `Int`. 3. Define an object `Printable` with two generic interface methods: @@ -29,36 +28,29 @@ These steps define the three main components of our type class. First we define `Printable`---the *type class* itself: ```scala mdoc:silent:reset-object -trait Printable[A] { +trait Printable[A]: def format(value: A): String -} ``` -Then we define some default *instances* of `Printable` -and package them in `PrintableInstances`: +Then we define some default *instances* of `Printable`: ```scala mdoc:silent -object PrintableInstances { - implicit val stringPrintable = new Printable[String] { - def format(input: String) = input - } +given stringPrintable: Printable[String] with + def format(input: String) = input - implicit val intPrintable = new Printable[Int] { - def format(input: Int) = input.toString - } -} +given intPrintable: Printable[Int] with + def format(input: Int) = input.toString ``` Finally we define an *interface* object, `Printable`: ```scala mdoc:silent -object Printable { - def format[A](input: A)(implicit p: Printable[A]): String = +object Printable: + def format[A](input: A)(using p: Printable[A]): String = p.format(input) - def print[A](input: A)(implicit p: Printable[A]): Unit = + def print[A](input: A)(using p: Printable[A]): Unit = println(p.format(input)) -} ``` @@ -104,16 +96,13 @@ These either go into the companion object of `Cat` or a separate object to act as a namespace: ```scala mdoc:silent -import PrintableInstances._ - -implicit val catPrintable = new Printable[Cat] { +given catPrintable: Printable[Cat] with def format(cat: Cat) = { val name = Printable.format(cat.name) val age = Printable.format(cat.age) val color = Printable.format(cat.color) s"$name is a $age year-old $color cat." } -} ``` Finally, we use the type class by @@ -135,45 +124,35 @@ Printable.print(cat) Let's make our printing library easier to use by defining some extension methods to provide better syntax: - 1. Create an object called `PrintableSyntax`. - - 2. Inside `PrintableSyntax` define an `implicit class PrintableOps[A]` - to wrap up a value of type `A`. + 1. Define an `extension [A](value: A)` to wrap up a value of type `A`. - 3. In `PrintableOps` define the following methods: + 2. Define the following extension methods: - - `format` accepts an implicit `Printable[A]` + - `format` using a `Printable[A]` and returns a `String` representation of the wrapped `A`; - - `print` accepts an implicit `Printable[A]` and returns `Unit`. + - `print` using a `Printable[A]` and returns `Unit`. It prints the wrapped `A` to the console. - 4. Use the extension methods to print the example `Cat` + 3. Use the extension methods to print the example `Cat` you created in the previous exercise.
-First we define an `implicit class` containing our extension methods: +First we define our extension methods: ```scala mdoc:silent -object PrintableSyntax { - implicit class PrintableOps[A](value: A) { - def format(implicit p: Printable[A]): String = - p.format(value) +extension [A](value: A) + def format(using p: Printable[A]): String = + p.format(value) - def print(implicit p: Printable[A]): Unit = - println(format(p)) - } -} + def print(using p: Printable[A]): Unit = + println(format(using p)) ``` -With `PrintableOps` in scope, +With the extensions in scope, we can call the imaginary `print` and `format` methods on any value for which Scala can locate an implicit instance of `Printable`: -```scala mdoc:silent -import PrintableSyntax._ -``` - ```scala mdoc Cat("Garfield", 41, "ginger and black").print ``` diff --git a/src/pages/type-classes/summary.md b/src/pages/type-classes/summary.md index 2f61c00c..9920935d 100644 --- a/src/pages/type-classes/summary.md +++ b/src/pages/type-classes/summary.md @@ -8,9 +8,9 @@ We saw the components that make up a type class: - A `trait`, which is the type class -- Type class instances, which are implicit values. +- Type class instances, which are given instances. -- Type class usage, which uses implicit parameters. +- Type class usage, which utilizes using clauses. We have also seen the general patterns in Cats type classes: diff --git a/yarn.lock b/yarn.lock index 01eba297..a30fc0d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -96,8 +96,8 @@ coffeeify@1.0.0: through "^2.3.6" coffeescript@^2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz" + version "2.7.0" + resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.7.0.tgz" combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8"