现代化的 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 实例。