I am trying to understand the traverse for list using this page, https://www.scala-exercises.org/cats/traverse
and I have a very basic question (sorry for those who think it is too easy or obvious). just check below signature
trait Traverse[F[_]] {
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}
import cats.data.{ NonEmptyList, OneAnd, Validated, ValidatedNel }
import cats.implicits._
def parseIntEither(s: String): Either[NumberFormatException, Int] =
Either.catchOnly[NumberFormatException](s.toInt)
List("1", "abc", "3").traverse(parseIntEither).isLeft should be(true)
you can see the signature traverse is expecting 2 arguments, the first argument to be a List, and the 2nd argument to be the 'f'. then why the above is calling it only with 1 argument? and instead the list becomes the 'instance'/'object' that the traverse method is called upon.
I am puzzled.
Thanks a lot.
Because if you have (n+1)-parameter method foo
class Arg1
class Arg2
object Obj {
def foo(arg1: Arg1, arg2: Arg2) = ???
}
you can define a syntax (extension method, eg with the same name foo
) delegating a call to the former foo
implicit class Arg1Ops(val arg1: Arg1) extends AnyVal {
def foo(arg2: Arg2) = Obj.foo(arg1, arg2)
}
// which is basically the same as
// class Arg1Ops(val arg1: Arg1) extends AnyVal {
// def foo(arg2: Arg2) = Obj.foo(arg1, arg2)
// }
//
// implicit def toArg1Ops(arg1: Arg1): Arg1Ops = new Arg1Ops(arg1)
and call foo
as a n-parameter method on the first parameter as a caller
val a1 = new Arg1
val a2 = new Arg2
a1.foo(a2)
In your example List("1", "abc", "3")
is like arg1
and parseIntEither
is like arg2
.
In Cats Traverse
is annotated with Simulacrum macro-annotation @typeclass
@typeclass trait Traverse[F[_]] extends Functor[F] with Foldable[F] with UnorderedTraverse[F] { ...
and this annotation generates a syntax for Traverse
object traverse extends TraverseSyntax
trait TraverseSyntax extends Traverse.ToTraverseOps
Traverse.scala
object Traverse extends scala.AnyRef with java.io.Serializable {
...
// macro-generated code
trait Ops[F[_], C] extends scala.AnyRef {
...
def traverse[G[_], B](f : scala.Function1[C, G[B]])(implicit
evidence$1 : cats.Applicative[G]
) : G[F[B]] = { /* compiled code */ }
...
}
// macro-generated code
trait ToTraverseOps extends scala.AnyRef {
@...
implicit def toTraverseOps[F[_], C](target : F[C])(implicit
tc : cats.Traverse[F]
) : Traverse.Ops[F, C] { type TypeClassType = cats.Traverse[F] } =
{ /* compiled code */ }
}
...
}
Consider the following equivalent ways of invoking traverse
implicitly[Traverse[List]].traverse(List("1", "abc", "3"))(parseIntEither) // scary
Traverse.apply[List].traverse(List("1", "abc", "3"))(parseIntEither) // a bit better
Traverse[List].traverse(List("1", "abc", "3"))(parseIntEither) // much better
List("1", "abc", "3").traverse(parseIntEither) // what I ideally want to write
In all cases we need to access the evidence that List
satisfies the constraints of Traverse
type class before we can call traverse
. We could be very explicit about it by using implicitly
as in the first case, but ideally we want to just invoke traverse
on a value without worrying too much about the type class mechanics as in the last case. The last case makes use of Scala facility called extension methods . Scala 3 has some new syntax which IMO really clarifies the extension concept. In Scala 2 they are implemented using implicit class
construct which perhaps is not very informative as a keyword to beginners first encountering the concept. In Scala 3 the concept of extension methods can be tied to the concept of type class directly, perhaps something like so
trait Traverse[F[_]]:
extension [G[_]: Applicative, A, B](fa: F[A]) def traverse(f: A => G[B]): G[F[B]]
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.