11

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

 3 years ago
source link: https://zhuanlan.zhihu.com/p/351275796
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在 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 实例。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK