Scalaz: how does `scalaz.syntax.applicative._` wor

2019-06-23 17:48发布

问题:

This question is related to this one, where I was trying to understand how to use the reader monad in Scala.

In the answer the autor uses the following code for getting an instance of ReaderInt[String]:

import scalaz.syntax.applicative._
val alwaysHello2: ReaderInt[String] = "hello".point[ReaderInt]

Which mechanisms does Scala use to resolve the type of the expression "hello".point[ReaderInt] so that it uses the right point function?

回答1:

A good first step any time you're trying to figure out something like this is to use the reflection API to desugar the expression:

scala> import scalaz.Reader, scalaz.syntax.applicative._
import scalaz.Reader
import scalaz.syntax.applicative._

scala> import scala.reflect.runtime.universe.{ reify, showCode }
import scala.reflect.runtime.universe.{reify, showCode}

scala> type ReaderInt[A] = Reader[Int, A]
defined type alias ReaderInt

scala> showCode(reify("hello".point[ReaderInt]).tree)
res0: String = `package`.applicative.ApplicativeIdV("hello").point[$read.ReaderInt](Kleisli.kleisliIdMonadReader)

(You generally don't want to use scala.reflect.runtime in real code, but it's extremely handy for investigations like this.)

When the compiler sees you trying to call .point[ReaderInt] on a type that doesn't have a point method—in this case String—it starts looking for implicit conversions that would convert a String into a type that does have a matching point method (this is called "enrichment" in Scala). We can see from the output of showCode that the implicit conversion it finds is a method called ApplicativeIdV in the applicative syntax object.

It then applies this conversion to the String, resulting in a value of type ApplicativeIdV[String]. This type's point method looks like this:

def point[F[_] : Applicative]: F[A] = Applicative[F].point(self)

Which is syntactic sugar for something like this:

def point[F[_]](implicit F: Applicative[F]): F[A] = F.point(self)

So the next thing it needs to do is find an Applicative instance for F. In your case you've explicitly specified that F is ReaderInt. It resolves the alias to Reader[Int, _], which is itself an alias for Kleisli[Id.Id, Int, _], and starts looking for an instance.

One of the first places it looks will be the Kleisli companion object, since it wants an implicit value of a type that includes Kleisli, and in fact showCode tells us that the one it finds is Kleisli.kleisliIdMonadReader. At that point it's done, and we get the ReaderInt[String] we wanted.



回答2:

I wanted to update the former answer, but since you created separate question, I put it here.

scalaz.syntax

Let's consider the point example, and you can apply the same reasoning for other methods.

point (or haskell's return) or pure(just a type alias) belongs to Applicative trait. If you want to put something inside some F, you need at least Applicative instance for this F.

Usually, you will provide it implicitly with imports, but you can specify it explicitly as well.

In example from the first question, I assigned it to val

implicit val KA = scalaz.Kleisli.kleisliIdApplicative[Int]

because scala was not able to figure out the corresponding Int type for this applicative. In other words, it did not know Applicative for which Reader to bring in. (though sometimes compiler can figure it out)

For the Applicatives with one type parameter, we can bring implicit instances in just by using import

import scalaz.std.option.optionInstance 
import scalaz.std.list.listInstance

etc...

Okay, you have the instance. Now you need to invoke point on it. You have few options:

1. Access method directly:

scalaz.std.option.optionInstance.point("hello")
KA.pure("hello")

2. Explicitly pull it from implicit context:

Applicative[Option].point("hello") 

If you look into Applicative object, you would see

object Applicative {
  @inline def apply[F[_]](implicit F: Applicative[F]): Applicative[F] = F
}

Implementation of apply, is only returning the corresponding Applicative[F] instance for some type F.

So Applicative[Option].point("hello") is converted to Applicative[Option].apply(scalaz.std.option.optionInstance) which in the end is just optionInstance

3. Use syntax

import scalaz.syntax.applicative._ 

brings this method into implicit scope:

  implicit def ApplicativeIdV[A](v: => A) = new ApplicativeIdV[A] {
    val nv = Need(v)
    def self = nv.value
  }

  trait ApplicativeIdV[A] extends Ops[A] {
    def point[F[_] : Applicative]: F[A] = Applicative[F].point(self)
    def pure[F[_] : Applicative]: F[A] = Applicative[F].point(self)
    def η[F[_] : Applicative]: F[A] = Applicative[F].point(self)
  }  ////

So then, whenever you try to invoke point on a String

"hello".point[Option] 

compiler realizes, that String does not have the method point and begins to look through implicits, how it can get something which has point, from String.

It finds, that it can convert String to ApplicativeIdV[String], which indeed has method point:

 def point[F[_] : Applicative]: F[A] = Applicative[F].point(self)

So in the end - your call desugares to

new ApplicativeIdV[Option]("hello")


More or less all typeclasses in scalaz are working the same way. For sequence, the implementation is

def sequence[G[_]: Applicative, A](fga: F[G[A]]): G[F[A]] =
    traverse(fga)(ga => ga)

This colon after G means, that Applicative[G] should be provided implicitly. It is essentialy the same as:

def sequence[G[_], A](fga: F[G[A]])(implicit ev: Applicative[G[_]]): G[F[A]] =
        traverse(fga)(ga => ga)

So all you need is the Applicative[G], and Traverse[F].

import scalaz.std.list.listInstance
import scalaz.std.option.optionInstance
Traverse[List].sequence[Option, String](Option("hello"))