现代化的 Java (三十六)——在 Scala 2中使用 typeclasses

在 Scala 2 中实现和使用 typeclasses

春节期间,我阅读了一些 scala 2 typeclasses 的文档,进一步学习了一些 implicit 相关的知识,基于 scala 2 ,对 jaskell typeclasses 功能做了一些尝试。
Scala 3 的 Typeclasses,实现路线很清晰,各种功能各司其职,特别是对泛型和隐式参数的支持非常的直观。而Scala 2 的实现就比较复杂。
当然,对 implicit 有一些了解后,会发现基本的结构仍然是类似的,只是 scala 2 的 implicit 其实代表了若干个不同的隐式转换规则。

我们可以在这里找到比较正式的 scala 2 typeclasses 文档: [
https://www.
baeldung.com/scala/type
-classes

] 。但是说实话这份文档对我来说几乎就是真空中的球形鸡。提供的帮助非常有限。在扩展 Parsec 时,我需要解决如何将两个类型参数的 Parsec 传递给 `Monad[M[_]]` 的问题——这里 M 代表接受一个类型参数的“容器”类型。而扩展 Future 时,我们还会遇到如何传入隐式参数 `ec: ExecutionContext `的问题。 如果说 Scala 3 基于 trait 、 typeclasses 和 given 的方案是一组精巧的机械,Scala 2的方案可以说是一个复杂的魔术机关。
首先,我们也要定义一个 Monad Trait ,因为懒得在 scala2 中死磕类型体系,我省去了 Functor/Applicative/Monad继承体系,直接定义了包含相关功能的 Monad trait。
我老了,现在更喜欢做一个凑合能用的东西,然后慢慢改进。

trait Monad[M[_]] {
  def pure[A](element: A): M[A]

  def fmap[A, B](m: M[A], f: A => B): M[B]

  def flatMap[A, B](m: M[A], f: A => M[B]): M[B]

  def liftA2[A, B, C](f: (A, B) => C): (Monad.MonadOps[A, M], Monad.MonadOps[B, M]) => M[C] = { (ma, mb) =>
    for {
      a <- ma
      b <- mb
    } yield f(a, b)
  }
}

这个 trait 包含了 functor 的 fmap,applicative 的 pure 和 liftA2, monad 的 flatMap ,但是没有包含更多实用化的代码,那些在 scala 3中,我通过 extension 实现的扩展方法,根据 scala 2 的实现规则,放在 `object Monad` 内部:

object Monad {
  ...

  abstract class MonadOps[A, M[_]](implicit I: Monad[M]) {
    def self: M[A]


    def map[B](f: A => B): M[B] = I.fmap(self, f)

    def [B](f: A => B): M[B] = I.fmap(self, f)

    def flatMap[B](f: A => M[B]): M[B] = I.flatMap(self, f)

    def liftA2[B, C](f: (A, B) => C): (M[B]) => M[C] = m => I.liftA2(f)(self, m)

    def [B](f: A => B): M[A] => M[B] = ma => I.fmap(ma, f)

    def *>[B](mb: M[B]): M[B] = for {
      _ <- self
      re <- mb
    } yield re

    def <*[_](mb: M[_]): M[A] = for {
      re <- self
      _ >=[B](f: A => M[B]): M[B] = flatMap(f)

    def >>[B](m: M[B]): M[B] = for {
      _ <- self
      re <- m
    } yield re

  }
  ...
}

我们暂时屏除其它代码,只看这个 MonadOps ,它定义了一个符合规则的 Monad 实例,可以拥有哪些扩展方法。
Ops 类型的扩展方法,通过 Monad 类型的 apply 方法,与对应的隐式变量实例配对:

object Monad {
  def apply[M[_]](implicit instance: Monad[M]): Monad[M] = instance

  ...
}

前述的文档中介绍了这个 apply 声明的用法,它可以为特定类型查找对应的实例,编译器通过下面的隐式函数将其组装成对应的 Ops 类型实例:

implicit def toMonad[A, M[_]](target: M[A])(implicit I: Monad[M]): MonadOps[A, M] =
    new MonadOps[A, M]() {
      override def self: M[A] = target
    }

例如我们在 Jaskell 中内置的 list 、 seq 和 try 的 monad 实例:

implicit val listMonad: Monad[List] = new Monad[List] {
    override def pure[A](element: A): List[A] = List(element)

    override def fmap[A, B](m: List[A], f: A => B): List[B] = m.map(f)

    override def flatMap[A, B](m: List[A], f: A => List[B]): List[B] = m.flatMap(f)
  }


  implicit val seqMonad: Monad[Seq] = new Monad[Seq] {
    override def pure[A](element: A): Seq[A] = Seq(element)

    override def fmap[A, B](m: Seq[A], f: A => B): Seq[B] = m.map(f)

    override def flatMap[A, B](m: Seq[A], f: A => Seq[B]): Seq[B] = m.flatMap(f)
  }

  implicit val tryMonad: Monad[Try] = new Monad[Try] {
    override def pure[A](element: A): Try[A] = Success(element)

    override def fmap[A, B](m: Try[A], f: A => B): Try[B] = m.map(f)

    override def flatMap[A, B](m: Try[A], f: A => Try[B]): Try[B] = m.flatMap(f)
  }

这些定义在 jaskell monad object 中: [
https://
github.com/MarchLiu/jas
kell-core/blob/master/src/main/scala/jaskell/Monad.scala

] 。
如前述的文档演示,这些单类型参数的 Monad 实现非常容易,而 Parsec 这样的类型,我们需要用到 scala 2的 type lambda,它比 scala 3的版本更冗长一些:

object Parsec {
  def apply[E, T](parser: State[E] => Try[T]): Parsec[E, T] = parser(_)

  implicit def toFlatMapper[E, T, O](binder: Binder[E, T, O]): (T)=>Parsec[E, O] = binder.apply

  implicit def mkMonad[T]: Monad[({type P[A] = Parsec[T, A]})#P] =
    new Monad[({type P[A] = Parsec[T, A]})#P] {
      override def pure[A](element: A): Parsec[T, A] = Return(element)

      override def fmap[A, B](m: Parsec[T, A], f: A => B): Parsec[T, B] = m.ask(_).map(f)

      override def flatMap[A, B](m: Parsec[T, A], f: A => Parsec[T, B]): Parsec[T, B] = state => for {
        a <- m.ask(state)
        b <- f(a).ask(state)
      } yield b
    }

}

这里我们的重点是利用 type lambda 定义了一个隐式的转换函数,将 Parsec 处理为 Monad 。它必须是一个 def 而非 val ,这是因为 val 需要所有类型参数的具体值,用来构造实例对象,它不能像 given 一样直接泛型化声明。所以我们给 monad 添加一个新的 apply 方法,用于从这样的隐式函数中得到具体的实例对象:

object Monad {
  ...
  def apply[M[_]](implicit creator: () => Monad[M]): Monad[M] = creator.apply()
  ...

这样,我们就可以在测试代码中验证我们的成果了:

class InjectionSpec extends AnyFlatSpec with Matchers {

  import jaskell.parsec.Atom.{one, eof}
  import jaskell.parsec.Combinator._
  import jaskell.parsec.Txt._
  import jaskell.parsec.Parsec._

  implicit def toParsec[E, T, P > ((s: State[Char]) => {
    s.next() flatMap {
      case 't' => Success('\t')
      case '\'' => Success('\'')
      case 'n' => Success('\n')
      case 'r' => Success('\r')
      case c@_ => Failure(new ParsecException(s.status, s"invalid escape char \\$c"))
    }
  }))
  val notEof: Parsec[Char, Char] = ahead(one[Char])

  val oneChar: Parsec[Char, Char] = escapeChar  nch('\'')

  val contentString: Parsec[Char, String] = ch('\'') *> many(oneChar) >= mkString

  val noString: Parsec[Char, String] = many1(nch('\'')) >>= mkString
  val content: Parsec[Char, String] = attempt(noString)  contentString

  val parser: Parsec[Char, String] = many(notEof >> content) >>= ((value: Seq[String]) => (s: State[Char]) => for {
      _ <- eof ? s
    } yield value.mkString)
...

坦诚的说,这样一来,Parsec 的功能与之前朴素的面向对象版本相比,是有所损失的,例如编译器一直不能自动推导出 `Many1[Char, Char]` 其实是 `Parsec[Char, Seq[Char]]` 的子类型,直到我修改了 Combinator object 中的 many 和 many1 的声明,使其返回 Parsec 类型:

def many[E, T](parser: Parsec[E, T]): Parsec[E, Seq[T]] = {
    new Many[E, T](parser)
  }

  def many1[E, T](parser: Parsec[E, T]): Parsec[E, Seq[T]] = {
    new Many1[E, T](parser)
  }

好在通过各种杂技,我基本上消除了这些问题。这样的收益在于,我们得到了一个更干净、更有弹性的类型体系,现在信息传递、变换功能与信息的结构正交的分解开。未来我们可以基于它实现一些对灵活性要求更高的东西,例如针对不同的SQL方言提供不同的SQL语法表达式。
解决了多类型参数的特化问题,接下来看 future 的 typeclasses 处理,这是另一个有趣的问题,构造 future 时,需要环境中包含隐式的 `ec: ExecutionContext` ,所以它也不能直接预定义为 Monad 实例,而是要借助我们前面定义的那个调用构造器得到monad实例的 apply 方法,在这里引入一个隐式函数:

implicit def toMonad(implicit ec: ExecutionContext): Monad[Future] = new Monad[Future] {
    override def pure[A](element: A): Future[A] = Future.successful(element)

    override def fmap[A, B](m: Future[A], f: A => B): Future[B] = m.map(f)

    override def flatMap[A, B](m: Future[A], f: A => Future[B]): Future[B] = m.flatMap(f)
  }

这样,我们就得到了 future 的 monad 实现支持:

package jaskell

import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.concurrent.Future

class FutureSpec extends AsyncFlatSpec with Matchers {
  import jaskell.Monad.toMonad

  val future: Future[Double] = Future("success") *> Future(3.14)  { value => value * 2*2}  value should be (3.14 * 2 * 2))
  }
}

如上例所示, future monad 引入了async spec 隐含的 execution context ,构造出了正确的 monad 实例。