现代化的 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
_ <- mb
} yield re
def >>=[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 <: Parsec[E, T]](parsec: P): Parsec[E, T] = parsec.asInstanceOf[Parsec[E, T]]
val escapeChar: Parsec[Char, Char] = attempt(ch('\\') >> ((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) <* ch('\'') >>= 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} <* Future("Right")
"Pi" should "success a area of r=2 circle" in {
future.map(value => value should be (3.14 * 2 * 2))
}
}

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

前路迢迢
我还没有学会写个人说明!
上一篇

西井与华为签署自动驾驶合作协议 携手助力天津港C段自动驾驶项目顺利完成首次联调

下一篇

微软发布.NET 6大版本的首个预览

你也可能喜欢

评论已经被关闭。

插入图片