(我原来的答案包含相同的想法,但显然它没有提供足够的实现细节。这一次,我编写了一个更详细的分步指南,并对每个中间步骤进行了讨论。每个部分都包含一个单独的可编译代码片段。 )
TL;DR
- 每种类型都需要隐式
T
发生在get[T]
,因此必须在 DSL 程序运行时插入并存储它们建,不是当它是executed。这解决了隐式问题。
- 有一个粘合自然变换的通用策略
~>
从几个有限的自然变换 trait RNT[R, F[_ <: R], G[_]]{ def apply[A <: R](x: F[A]): G[A] }
使用模式匹配。这解决了问题A <: Resource
类型绑定。详细信息如下。
在您的问题中,您有两个独立的问题:
- 隐含的
Format
and Definition
-
<: Resource
-类型绑定
我想单独处理这两个问题,并为这两个问题提供可重用的解决方案策略。然后我会将这两种策略应用于您的问题。
我的回答结构如下:
- 首先,我按照我的理解总结一下你的问题。
- 然后我将解释如何处理隐式,忽略类型绑定。
- 然后我将处理类型绑定,这次忽略隐式。
- 最后,我将这两种策略应用于您的特定问题。
从今往后,我假设你已经scalaVersion
2.12.4
,依赖关系
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.1"
libraryDependencies += "org.typelevel" %% "cats-free" % "1.0.1"
并且您插入
import scala.language.higherKinds
在适当情况下。
请注意,解决方案策略并不特定于该特定的 scala 版本或cats
图书馆。
设置
本节的目标是确保我解决正确的问题,并提供非常简单的模型定义
的Resource
, Format
, Client
等等,所以这个答案是独立的
并且可以编译。
我假设您想使用以下方法构建一种特定于领域的语言Free
单子。
理想情况下,您希望有一个看起来大约像这样的 DSL(我使用的名称DslOp
用于操作和Dsl
对于生成的免费 monad):
import cats.free.Free
import cats.free.Free.liftF
sealed trait DslOp[A]
case class Get[A](name: String) extends DslOp[A]
type Dsl[A] = Free[DslOp, A]
def get[A](name: String): Dsl[A] = liftF[DslOp, A](Get[A](name))
它定义了一个命令get
可以获取类型的对象A
给定一个字符串
姓名。
稍后,您想使用get
一些人提供的方法Client
您无法修改:
import scala.concurrent.Future
trait Resource
trait Format[A <: Resource]
trait Definition[A <: Resource]
object Client {
def get[A <: Resource](name: String)
(implicit f: Format[A], d: Definition[A]): Future[A] = ???
}
你的问题是get
的方法Client
有一个类型绑定,并且
它需要额外的隐式。
为 Free monad 定义解释器时处理隐式
我们首先假设get
- 客户端中的方法需要隐式,但是
暂时忽略类型绑定:
import scala.concurrent.Future
trait Format[A]
trait Definition[A]
object Client {
def get[A](name: String)(implicit f: Format[A], d: Definition[A])
: Future[A] = ???
}
在我们写下解决方案之前,让我们简单讨论一下为什么你不能提供所有的
当您调用时必要的隐式apply
中的方法~>
.
当传递到foldMap
, the apply
of FunctionK
应该
能够处理任意长的程序类型Dsl[X]
生产Future[X]
.
任意长的程序类型Dsl[X]
可以包含无限数量的get[T1]
, ..., get[Tn]
不同类型的命令T1
, ..., Tn
.
对于每一个T1
, ..., Tn
,你必须得到一个Format[T_i]
and Definition[T_i]
某处。
这些隐式参数必须由编译器提供。
当你解释整个类型的程序时Dsl[X]
,仅类型X
但不是类型T1
, ..., Tn
可用,
所以编译器无法插入所有必需的Definition
s and Format
位于呼叫站点。
因此,所有的Definition
s and Format
s 必须作为隐式参数提供给get[T_i]
当你是建造 the Dsl
- 程序,而不是当你在的时候口译 it.
解决方案是添加Format[A]
and Definition[A]
作为成员Get[A]
案例类,
并定义get[A]
with lift[DslOp, A]
接受这两个额外的隐式
参数:
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
sealed trait DslOp[A]
case class Get[A](name: String, f: Format[A], d: Definition[A])
extends DslOp[A]
type Dsl[A] = Free[DslOp, A]
def get[A](name: String)(implicit f: Format[A], d: Definition[A])
: Dsl[A] = liftF[DslOp, A](Get[A](name, f, d))
现在,我们可以定义第一个近似值~>
-口译员,至少
可以应对隐式:
val clientInterpreter_1: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case Get(name, f, d) => Client.get(name)(f, d)
}
}
定义 DSL 操作的案例类中的类型界限
现在,让我们单独处理类型绑定。假设你的Client
不需要任何隐式,但施加了额外的限制A
:
import scala.concurrent.Future
trait Resource
object Client {
def get[A <: Resource](name: String): Future[A] = ???
}
如果你尝试写下clientInterpreter
与中相同的方式
在前面的示例中,您会注意到类型A
太笼统了,而且
因此您无法使用以下内容Get[A]
in Client.get
。
相反,您必须找到一个范围,其中附加类型信息A <: Resource
没有丢失。实现它的一种方法是定义一个accept
方法上Get
本身。
而不是完全一般的自然变换~>
, this accept
方法将
能够与有限域的自然变换。
这是一个可以建模的特征:
trait RestrictedNat[R, F[_ <: R], G[_]] {
def apply[A <: R](fa: F[A]): G[A]
}
看起来几乎就像~>
,但还有一个额外的A <: R
限制。现在我们
可以定义accept
in Get
:
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
sealed trait DslOp[A]
case class Get[A <: Resource](name: String) extends DslOp[A] {
def accept[G[_]](f: RestrictedNat[Resource, Get, G]): G[A] = f(this)
}
type Dsl[A] = Free[DslOp, A]
def get[A <: Resource](name: String): Dsl[A] =
liftF[DslOp, A](Get[A](name))
并写下我们解释器的第二个近似值,没有任何
讨厌的类型转换:
val clientInterpreter_2: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case g @ Get(name) => {
val f = new RestrictedNat[Resource, Get, Future] {
def apply[X <: Resource](g: Get[X]): Future[X] = Client.get(g.name)
}
g.accept(f)
}
}
}
这个想法可以推广到任意数量的类型构造函数Get_1
, ...,
Get_N
,有类型限制R1
, ..., RN
。总体思路对应于
从较小的分段定义的自然变换的构造
仅适用于某些子类型的作品。
将两种解决策略应用于您的问题
现在我们可以将两种通用策略结合成一种解决方案
你的具体问题:
import scala.concurrent.Future
import cats.free.Free
import cats.free.Free.liftF
import cats.~>
// Client-definition with both obstacles: implicits + type bound
trait Resource
trait Format[A <: Resource]
trait Definition[A <: Resource]
object Client {
def get[A <: Resource](name: String)
(implicit fmt: Format[A], dfn: Definition[A])
: Future[A] = ???
}
// Solution:
trait RestrictedNat[R, F[_ <: R], G[_]] {
def apply[A <: R](fa: F[A]): G[A]
}
sealed trait DslOp[A]
case class Get[A <: Resource](
name: String,
fmt: Format[A],
dfn: Definition[A]
) extends DslOp[A] {
def accept[G[_]](f: RestrictedNat[Resource, Get, G]): G[A] = f(this)
}
type Dsl[A] = Free[DslOp, A]
def get[A <: Resource]
(name: String)
(implicit fmt: Format[A], dfn: Definition[A])
: Dsl[A] = liftF[DslOp, A](Get[A](name, fmt, dfn))
val clientInterpreter_3: (DslOp ~> Future) = new (DslOp ~> Future) {
def apply[A](op: DslOp[A]): Future[A] = op match {
case g: Get[A] => {
val f = new RestrictedNat[Resource, Get, Future] {
def apply[X <: Resource](g: Get[X]): Future[X] =
Client.get(g.name)(g.fmt, g.dfn)
}
g.accept(f)
}
}
}
现在clientInterpreter_3
可以解决这两个问题:处理类型绑定问题
通过定义一个RestrictedNat
对于每个对其类型参数施加上限的案例类,
通过向 DSL 添加隐式参数列表来解决隐式问题get
-method.