使用上下文界定“负面”地确保类型类实例不在作用域中

19

简短概述: 我如何实现下面这个虚构代码的功能:

def notFunctor[M[_] : Not[Functor]](m: M[_]) = s"$m is not a functor"

这里的'Not[Functor]'是虚构的部分。
我希望当提供的'm'不是一个Functor时成功,并且否则使编译器失败。

解决方案: 跳过问题的其余部分,直接查看下面的答案。


我的目标大致上是实现"负证据"。

伪代码如下:

// type class for obtaining serialization size in bytes.
trait SizeOf[A] { def sizeOf(a: A): Long }

// type class specialized for types whose size may vary between instances
trait VarSizeOf[A] extends SizeOf[A]

// type class specialized for types whose elements share the same size (e.g. Int)
trait FixedSizeOf[A] extends SizeOf[A] {
  def fixedSize: Long
  def sizeOf(a: A) = fixedSize
}

// SizeOf for container with fixed-sized elements and Length (using scalaz.Length)
implicit def fixedSizeOf[T[_] : Length, A : FixedSizeOf] = new VarSizeOf[T[A]] {
  def sizeOf(as: T[A]) = ... // length(as) * sizeOf[A]
}

// SizeOf for container with scalaz.Foldable, and elements with VarSizeOf
implicit def foldSizeOf[T[_] : Foldable, A : SizeOf] = new VarSizeOf[T[A]] {
  def sizeOf(as: T[A]) = ... // foldMap(a => sizeOf(a))
}

请记住,在相关情况下,使用fixedSizeOf()是更可取的,因为它节省了我们对集合的遍历。
这样,在仅定义了Length(但未定义Foldable)的容器类型和定义了FixedSizeOf的元素中,我们可以获得更高的性能。
对于其余情况,我们遍历集合并计算每个元素的大小之和。
我的问题在于那些容器类型既定义了Length又定义了Foldable,而元素则定义了FixedSizeOf。这在这里是非常普遍的情况(例如:List[Int]就同时定义了这两个属性)。
示例:
scala> implicitly[SizeOf[List[Int]]].sizeOf(List(1,2,3))
<console>:24: error: ambiguous implicit values:
 both method foldSizeOf of type [T[_], A](implicit evidence$1: scalaz.Foldable[T], implicit evidence$2: SizeOf[A])VarSizeOf[T[A]]
 and method fixedSizeOf of type [T[_], A](implicit evidence$1: scalaz.Length[T], implicit evidence$2: FixedSizeOf[A])VarSizeOf[T[A]]
 match expected type SizeOf[List[Int]]
              implicitly[SizeOf[List[Int]]].sizeOf(List(1,2,3))

我希望能够在Length+FixedSizeOf组合不适用时仅依赖于Foldable类型类。
为此,我可以更改foldSizeOf()的定义以接受VarSizeOf元素。
implicit def foldSizeOfVar[T[_] : Foldable, A : VarSizeOf] = // ...

现在我们需要填写覆盖具有固定大小元素的可折叠容器和未定义长度的问题部分。我不确定如何处理这个问题,但伪代码可能如下:

implicit def foldSizeOfFixed[T[_] : Foldable : Not[Length], A : FixedSizeOf] = // ...

"Not[Length]"是显然虚构的部分。
我知道的部分解决方法:
1)定义一个用于低优先级隐式的类并扩展它,就像在“object Predef extends LowPriorityImplicits”中看到的那样。最后一个隐式(foldSizeOfFixed())可以在父类中定义,并将被子类中的替代方案覆盖。
我不感兴趣这个选项,因为我希望最终能够支持SizeOf的递归使用,而这将阻止低优先级基类中的隐式依赖于子类中的隐式(我的理解是否正确?编辑:不对!隐式查找从子类的上下文中工作,这是一个可行的解决方案!)
2)一种更粗暴的方法是依赖于Option[TypeClass](例如:Option[Length[List]])。有几个这样的实例,我只需编写一个大的隐式,将FoldableSizeOf作为强制性参数选择,将LengthFixedSizeOf作为可选参数选择,如果它们可用则依赖于后者。(来源:这里
这里的两个问题是缺乏模块化和当找不到相关的类型类实例时回退到运行时异常(这个例子可能可以用这个解决方案,但这并不总是可能)。
编辑:这是我能够通过可选隐式获得的最佳结果。还不够好。
implicit def optionalTypeClass[TC](implicit tc: TC = null) = Option(tc)
type OptionalLength[T[_]] = Option[Length[T]]
type OptionalFixedSizeOf[T[_]] = Option[FixedSizeOf[T]]

implicit def sizeOfContainer[
    T[_] : Foldable : OptionalLength,
    A : SizeOf : OptionalFixedSizeOf]: SizeOf[T[A]] = new SizeOf[T[A]] {
  def sizeOf(as: T[A]) = {

    // optionally calculate using Length + FixedSizeOf is possible
    val fixedLength = for {
      lengthOf <- implicitly[OptionalLength[T]]
      sizeOf <- implicitly[OptionalFixedSizeOf[A]]
    } yield lengthOf.length(as) * sizeOf.fixedSize

    // otherwise fall back to Foldable
    fixedLength.getOrElse { 
      val foldable = implicitly[Foldable[T]]
      val sizeOf = implicitly[SizeOf[A]]
      foldable.foldMap(as)(a => sizeOf.sizeOf(a))
    }
  }
}

但是这与之前的fixedSizeOf()相冲突,后者仍然是必要的。

感谢任何帮助或观点 :-)


以上的选项#1(具有隐式优先级)将起作用。我仍然对更加优雅的解决方案感兴趣,因此问题现在仍然是开放的。 - nadavwr
1
也许你可以详细说明一下“否定技巧(miles answer, update)”(https://dev59.com/52w05IYBdhLWcg3w_Gqw#6944070) - Peter Schmitz
1
否定技巧可能在这里能够发挥作用,但我认为在这种情况下优先级是更可取的解决方案。 - Miles Sabin
我添加了一个答案,然后重新阅读问题,意识到我错过了一个重要部分,所以将其删除了。 - Impredicative
缺乏证据并不意味着证据不存在! - Randall Schulz
2个回答

17
我最终使用了一种基于歧义的解决方案,它不需要使用继承来进行优先级排序。
以下是我的概括尝试。
我们使用类型Not[A]来构建负面类型类:
import scala.language.higherKinds

trait Not[A]

trait Monoid[_] // or import scalaz._, Scalaz._
type NotMonoid[A] = Not[Monoid[A]] 

trait Functor[_[_]] // or import scalaz._, Scalaz._
type NotFunctor[M[_]] = Not[Functor[M]]

...然后可以用作上下文边界:

def foo[T: NotMonoid] = ...

我们需要确保每个有效的Not[A]表达式都至少获得一个隐式实例。
implicit def notA[A, TC[_]] = new Not[TC[A]] {}

该实例被称为“notA”——“not”是因为如果它是“Not [TC [A]]”找到的唯一实例,则会发现负类型类适用;“A”通常附加在处理扁平形状类型(例如Int)的方法上。
我们现在引入了一个歧义,以避免应用不需要的类型类。
implicit def notNotA[A : TC, TC[_]] = new Not[TC[A]] {}

这个和“NotA”几乎完全相同,只不过我们只对那些存在于隐式作用域中指定的类型类实例的类型感兴趣。该实例被命名为“notNotA”,因为仅通过匹配正在查找的隐式参数,它将与“notA”引起歧义,从而失败隐式搜索(这是我们的目标)。
让我们看一个使用示例。我们将使用上面的“NotMonoid”负类型类:
implicitly[NotMonoid[java.io.File]] // succeeds
implicitly[NotMonoid[Int]] // fails

def showIfNotMonoid[A: NotMonoid](a: A) = a.toString

showIfNotMonoid(3) // fails, good!
showIfNotMonoid(scala.Console) // succeeds for anything that isn't a Monoid

到目前为止一切都很好!然而,形状为M[_]的类型和形状为TC[_[_]]的类型类还没有被上述方案支持。让我们也为它们添加隐式转换:

implicit def notM[M[_], TC[_[_]]] = new Not[TC[M]] {}
implicit def notNotM[M[_] : TC, TC[_[_]]] = new Not[TC[M]] {}

implicitly[NotFunctor[List]] // fails
implicitly[NotFunctor[Class]] // succeeds

很简单。请注意,Scalaz针对处理多个类型形状所导致的样板代码问题有一个解决方法——请查找“Unapply”。尽管我无法将其用于基本情况(形状TC [_]的类型类,例如Monoid),但它在TC [_[_]] (例如Functor)上运行良好,因此该答案不涵盖这一点。

如果有人感兴趣,以下是一个单个片段中所需的所有内容:

import scala.language.higherKinds

trait Not[A]

object Not {
  implicit def notA[A, TC[_]] = new Not[TC[A]] {}
  implicit def notNotA[A : TC, TC[_]] = new Not[TC[A]] {}

  implicit def notM[M[_], TC[_[_]]] = new Not[TC[M]] {}
  implicit def notNotM[M[_] : TC, TC[_[_]]] = new Not[TC[M]] {}
}

import Not._

type NotNumeric[A] = Not[Numeric[A]]
implicitly[NotNumeric[String]] // succeeds
implicitly[NotNumeric[Int]] // fails

我需要的伪代码如下所示(实际代码):

// NotFunctor[M[_]] declared above
def notFunctor[M[_] : NotFunctor](m: M[_]) = s"$m is not a functor"

更新:类似的技术也可以应用于隐式转换:

import scala.language.higherKinds

trait Not[A]

object Not {
  implicit def not[V[_], A](a: A) = new Not[V[A]] {}
  implicit def notNot[V[_], A <% V[A]](a: A) = new Not[V[A]] {}
}

现在我们可以定义一个函数,仅在类型不可视为Ordered时才接受值:
def unordered[A <% Not[Ordered[A]]](a: A) = a

我之前发布的解决方案使用了'HasNo[M[_], TC[_[_]]]'二元类型构造器(中缀使用:'List HasNo Functor'),但是在不引入新标识符的情况下扩展以支持'A'和'TC[_]'似乎是不可能的。最终,我选择了上面的'Not[A]'类型,并使用了几个隐式函数将'A'分解为其组件(M[_]和TC[_[_]]或A和TC[_])。 - nadavwr
虽然我喜欢编译器检测大部分错误和/或早期决定算法分支的情况,即使我实际上同意你这一点肯定很方便 - 它仍然是一种..糖果。大多数人可能会选择更简单的路线。小心不要被指责为http://c2.com/cgi/wiki?MentalMasturbation ;-) 做得好!继续加油!;-) - quetzalcoatl
1
@quetzalcoatl 针对这个有 StackOverflow 徽章吗? - nadavwr
显然没有,没有明确的规则,人们需要投票决定 :) 不过回答自己的问题会获得一些徽章,我认为它们是最接近的,就像解决了自己的问题,而没有人想深入研究?不过我不确定。 - quetzalcoatl
@quetzalcoatl 开玩笑的话,你对类型级编程持怀疑态度吗?我的使用案例是序列化,让编译器为对象找到最佳序列化策略是非常有益的。 - nadavwr
不是怀疑,只是有点苦涩。我喜欢C2 Wiki,最近看到了那篇MM文章,与“思考会伤害你”的一般想法有些关联。当然,MM意味着极端和不必要的代码复杂化,这显然不适用于这里,但我知道许多非初学者程序员会将你的工作描述为这样。我倾向于限制自己创建像这样的工具,并努力使它们简单易懂,但我听说X或Y那家伙不喜欢和我一起工作,因为“我强迫他们思考” :) 我完全同意/祝贺/感谢/+1/确认你的工作! GJ / KeepOn很认真 :) - quetzalcoatl

1
在Scala 3(又名Dotty)中,先前提到的技巧不再奏效。使用NotGiven内置了否定given
def f[T](value: T)(using ev: NotGiven[MyTypeclass[T]])

例子:

f("ok") // no given instance of MyTypeclass[T] in scope

given MyTypeclass[String] = ... // provide the typeclass
f("bad") // compile error

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接