【问题标题】:Enforce type difference强制类型差异
【发布时间】:2011-10-18 01:16:18
【问题描述】:

在 Scala 中,我可以在编译时强制执行类型相等。例如:

case class Foo[A,B]( a: A, b: B )( implicit ev: A =:= B )

scala> Foo( 1, 2 )
res3: Foo[Int,Int] = Foo(1,2)

scala> Foo( 1, "2" )
<console>:10: error: Cannot prove that Int =:= java.lang.String.

有没有办法强制类型 A 和类型 B 应该不同?

【问题讨论】:

  • 不错……但它有有用的应用吗?听起来像 Miles Sabin's magical typesystem tricks 这样的东西可能有用。
  • @Jean-Phillipe_Pellet:我想制作类型 [A,B] 的双向映射,当我用 A 调用 apply 时,它返回一个 B,当我用 B 调用 apply 时它返回一个A。我不知道这是否可能,但至少我需要不同的类型。
  • 我明白了。您可能不想以不同的方式命名这两个访问器方法,以允许相同的类型参数吗?
  • 如果我找不到实现第一个想法的方法,那将是 B 计划。
  • 有趣。我认为类似于implicit def t[N, A &gt;: N, B &gt;: N]()(implicit n: N =:= Nothing) = new =/=[A,B] 的东西会起作用(表达“如果 A 和 B 的最接近的公共子类型是 Nothing,它们是不同的”),但它没有......

标签: scala types


【解决方案1】:

我有一个更简单的解决方案,它也利用了歧义,

trait =!=[A, B]

implicit def neq[A, B] : A =!= B = null

// This pair excludes the A =:= B case
implicit def neqAmbig1[A] : A =!= A = null
implicit def neqAmbig2[A] : A =!= A = null

原始用例,

case class Foo[A,B](a : A, b : B)(implicit ev: A =!= B)
new Foo(1, "1")
new Foo("foo", Some("foo"))

// These don't compile
// new Foo(1, 1)
// new Foo("foo", "foo")
// new Foo(Some("foo"), Some("foo"))

更新

我们可以将其与我的"magical typesystem tricks" 联系起来(感谢@jpp ;-),如下所示,

type ¬[T] = T => Nothing
implicit def neg[T, U](t : T)(implicit ev : T =!= U) : ¬[U] = null

def notString[T <% ¬[String]](t : T) = t

REPL 会话示例,

scala> val ns1 = notString(1)
ns1: Int = 1

scala> val ns2 = notString(1.0)
ns2: Double = 1.0

scala> val ns3 = notString(Some("foo"))
ns3: Some[java.lang.String] = Some(foo)

scala> val ns4 = notString("foo")
<console>:14: error: No implicit view available from 
  java.lang.String => (String) => Nothing.
       val ns4 = notString2("foo")
                            ^

【讨论】:

  • 不错!可能值得指出的是,与其他答案一样,这实现了 negation-as-failure: notString("foo": AnyRef) 编译。
  • @Aaron 这种技术,实际上是这个问题,只在静态环境中才有意义。否则除了进行动态运行时类型测试之外别无选择。
  • @Miles 我想知道是否有一种方法可以静态强制执行更强的不等式概念,其中静态约束表明类型的任何两个实例具有不同的运行时类型。例如,String 和 Int 在这个意义上是不相等的,而 String 和 AnyRef 则不是。 String with Int 似乎是一个矛盾,但是,如果我的理解是正确的,它是不可实例化的。
  • @Aaron 不完全确定您的意思,但相关地可以定义一个类型运算符 A <: b a>
  • @Miles 假设我想定义=!= 以便A =!= B 当且仅当A 的每个非空实例都不是B 的实例并且每个非空实例B 不是 A 的实例。根据这个定义,String =!= Int。我的直觉是编译器拥有强制执行约束的信息,但它在 Scala 中无法表达。
【解决方案2】:

借鉴 Jean-Philippe 的想法,这是可行的:

sealed class =!=[A,B]

trait LowerPriorityImplicits {
  implicit def equal[A]: =!=[A, A] = sys.error("should not be called")
}
object =!= extends LowerPriorityImplicits {
  implicit def nequal[A,B](implicit same: A =:= B = null): =!=[A,B] = 
    if (same != null) sys.error("should not be called explicitly with same type")
    else new =!=[A,B]
}     

case class Foo[A,B](a: A, b: B)(implicit e: A =!= B)

然后:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

我可能会将其简化如下,因为无论如何都可以绕过对“作弊”的检查(例如Foo(1, 1)(null)=!=.nequal(null)):

sealed class =!=[A,B]

trait LowerPriorityImplicits {
  /** do not call explicitly! */
  implicit def equal[A]: =!=[A, A] = sys.error("should not be called")
}
object =!= extends LowerPriorityImplicits {
  /** do not call explicitly! */
  implicit def nequal[A,B]: =!=[A,B] = new =!=[A,B]
}

【讨论】:

  • 谢谢!我在博客评论中发现了类似的东西,但不幸的是,检查发生在运行时而不是编译时。
  • @paradigmatic 我不确定我是否正确解释了您的评论....在我的代码中,检查发生在编译时——您的意思是博客评论演示了运行时检查?
  • 我的错。您的解决方案完美运行!我已经理解与我所见的区别。我用子类型检查了它,它也很好用。
  • @paradigmatic 更新了一个更简单的版本,消除了“作弊”检查。正如答案中所解释的那样,无论如何,这些检查总是可以绕过的。
  • @Aaron 很好的简化。您可以声明sealed class =!=[A,B] extends NotNull 以防止讨厌的用户手动传递null。同样,我们可以有一个=:=NotNull 子类来在您的第一个版本中执行作弊检查。我们不能阻止的最后一件事是手动调用equal...
【解决方案3】:

我喜欢 Miles Sabin 的第一个解决方案的简单性和有效性,但对我们得到的错误不是很有帮助这一事实有点不满意:

以如下定义为例:

def f[T]( implicit e: T =!= String ) {}

尝试执行f[String] 将无法编译:

<console>:10: error: ambiguous implicit values:
 both method neqAmbig1 in object =!= of type [A]=> =!=[A,A]
 and method neqAmbig2 in object =!= of type [A]=> =!=[A,A]
 match expected type =!=[String,String]
              f[String]
               ^

我宁愿让编译器告诉我一些类似“T 与字符串没有区别”的内容 事实证明,如果添加另一个级别的隐式以使 ambiguity 错误发生,这很容易 隐式未找到错误。从那时起,我们可以使用implicitNotFound 注释来发出自定义错误消息:

@annotation.implicitNotFound(msg = "Cannot prove that ${A} =!= ${B}.")
trait =!=[A,B]
object =!= {
  class Impl[A, B]
  object Impl {
    implicit def neq[A, B] : A Impl B = null
    implicit def neqAmbig1[A] : A Impl A = null
    implicit def neqAmbig2[A] : A Impl A = null
  }

  implicit def foo[A,B]( implicit e: A Impl B ): A =!= B = null
}

现在让我们尝试拨打f[String]

scala> f[String]
<console>:10: error: Cannot prove that String =!= String.
              f[String]
           ^

这样更好。感谢编译器。

对于那些喜欢上下文绑定语法糖的人来说,最后一个技巧是,可以定义这个别名(基于类型 lambda):

type IsNot[A] = { type λ[B] = A =!= B }

那么我们可以这样定义f

def f[T:IsNot[String]#λ] {}

是否更容易阅读是高度主观的。在任何情况下都比编写完整的隐式参数列表要短。

更新:为了完整起见,这里表示A 不是B 的子类型的等效代码:

@annotation.implicitNotFound(msg = "Cannot prove that ${A} <:!< ${B}.")
trait <:!<[A,B]
object <:!< {
  class Impl[A, B]
  object Impl {
    implicit def nsub[A, B] : A Impl B = null
    implicit def nsubAmbig1[A, B>:A] : A Impl B = null
    implicit def nsubAmbig2[A, B>:A] : A Impl B = null
  }

  implicit def foo[A,B]( implicit e: A Impl B ): A <:!< B = null
}

type IsNotSub[B] = { type λ[A] = A <:!< B }

为了表达 A 不能转换为 B

@annotation.implicitNotFound(msg = "Cannot prove that ${A} <%!< ${B}.")
trait <%!<[A,B]
object <%!< {
  class Impl[A, B]
  object Impl {
    implicit def nconv[A, B] : A Impl B = null
    implicit def nconvAmbig1[A<%B, B] : A Impl B = null
    implicit def nconvAmbig2[A<%B, B] : A Impl B = null
  }

  implicit def foo[A,B]( implicit e: A Impl B ): A <%!< B = null
}

type IsNotView[B] = { type λ[A] = A <%!< B }

【讨论】:

  • 这是最好的答案! 2021 年刚刚在 Scala 2.12 上运行。
  • 我试图阻止 Futures 被传递到我的计时方法中,这是签名:def time[T:IsNotSub[Future[Any]]#λ](reporting: Double =&gt; Unit, block: =&gt; T)
  • 不错。尽管可以说在这种情况下,您可能只想将未来作为一种特殊情况来处理,而不是完全禁止未来通过(使用单独的重载)并在未来完成后获取结束时间。
【解决方案4】:

基于Landei 的想法,以下似乎可行:

case class Foo[A, B <: A, C <: A]( a: B, b: C)(implicit f: AnyVal <:< A)

scala> Foo(1f, 1.0)
res75: Foo[AnyVal,Float,Double] = Foo(1.0,1.0)

scala> Foo("", 1.0)
res76: Foo[Any,java.lang.String,Double] = Foo(,1.0)

scala> Foo(1f, 1f)
<console>:10: error: Cannot prove that AnyVal <:< Float.
       Foo(1f, 1f)
          ^

scala> Foo("", "")
<console>:10: error: Cannot prove that AnyVal <:< java.lang.String.
       Foo("", "")
          ^

scala> Foo("", 1)
res79: Foo[Any,java.lang.String,Int] = Foo(,1)

【讨论】:

  • 好招!但这仅适用于原始类型,并且仅因为那里的层次结构是平的。这不起作用:Foo("Fish",Some("Fish")).
【解决方案5】:

这是另一个尝试:

class =!=[A, B] private () extends NotNull

object =!= {
  implicit def notMeantToBeCalled1[A, B >: A, C >: B <: A]: =!=[B, A] = error("should not be called")
  implicit def notMeantToBeCalled2[A, B >: A, C >: B <: A]: =!=[B, A] = error("should not be called")
  implicit def unambigouslyDifferent[A, B](implicit same: A =:= B = null): =!=[A, B] =
    if (same != null) error("should not be called explicitly with the same type")
    else new =!=
}

case class Foo[A, B](a: A, b: B)(implicit ev: A =!= B)

然后,再次:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

与我的其他建议一样,这里的目的是在 AB 相同时引入编译时歧义。在这里,我们为AB 相同的情况提供了两个隐式,当不是这种情况时,我们提供了一个明确的隐式。

请注意,问题在于您仍然可以通过手动调用=!=.notMeantToBeCalled1=!=.unambigouslyDifferent 来显式提供隐式参数。我想不出在编译时防止这种情况的方法。但是,我们可以在运行时抛出异常,其中unambigouslyDifferent 需要一个证据参数本身来指示A 是否与B 相同。但是等等……我们不是想证明完全相反的吗?是的,这就是为什么same 隐式参数的默认值为null。我们希望它是 null 用于所有合法用途 - 唯一不是 null 的情况是当一个讨厌的用户打电话时,例如Foo(1f, 1f)(=:=.unambiguouslyDifferent[Float, Float]),我们可以通过抛出异常来防止这种作弊。

【讨论】:

  • 我不确定作弊检查是否值得额外的复杂性 - 特别是因为讨厌的用户仍然可以调用 =!=.unambiguoslyDifferent[Float, Float](null),或者,因为 =!= 只是一个幻像类型,Foo(1, 1)(null) .如果 Scala 有某种方式来表示不能显式调用隐式方法,那会很好
【解决方案6】:

那么,这样的事情怎么样?

class Foo[A, B] private (a: A, b: B)

object Foo {
  def apply[A, B <: A, C >: A <: B](a: A, b: B)(implicit nothing: Nothing) = nothing
  def apply[A, B >: A, C >: B <: A](a: A, b: B)(implicit nothing: Nothing, dummy: DummyImplicit) = nothing
  def apply[A, B](a: A, b: B): Foo[A, B] = new Foo(a, b)
}

然后:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

这个想法是在AB相同时使分辨率不明确,而在它们不同时明确。为了进一步强调不应该调用模棱两可的方法,我添加了一个 Nothing 类型的隐式方法,它永远不应该存在(如果调用者试图显式插入一个,它肯定会看起来不对)。 (DummyImplicit的作用只是给前两种方法不同的签名。)

【讨论】:

    【解决方案7】:

    这不是答案,只是我能想到的答案的开始。如果您要求implicitly[AreEqual[A,B]],下面的代码将返回YesNo,具体取决于类型是否相等。如何从那里实际进行检查,我无法弄清楚。也许整个方法注定要失败,也许有人可以从中有所作为。请注意,implicitly[No[A, B]]总是返回一些东西,一个人不能使用它。 :-(

    class AreEqual[A, B]
    trait LowerPriorityImplicits {
      implicit def toNo[A : Manifest, B : Manifest]: No[A, B] = No[A, B]
    }
    object AreEqual extends LowerPriorityImplicits {
      implicit def toYes[A, B](implicit ev: A =:= B, m1: Manifest[A], m2: Manifest[B]): Yes[A, B] = Yes[A, B]
    }
    
    case class Yes[A : Manifest, B : Manifest]() extends AreEqual[A, B] {
      override def toString: String = "Yes(%s, %s)" format (manifest[A].toString, manifest[B].toString)
    }
    case class No[A : Manifest, B : Manifest]() extends AreEqual[A, B] {
      override def toString: String = "No(%s, %s)" format (manifest[A].toString, manifest[B].toString)
    }
    

    测试:

    scala> implicitly[AreEqual[String, Option[String]]]
    res0: AreEqual[String,Option[String]] = No(java.lang.String, scala.Option[java.lang.String])
    
    scala> implicitly[AreEqual[String, String]]
    res1: AreEqual[String,String] = Yes(java.lang.String, java.lang.String)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-27
      • 2016-05-10
      • 1970-01-01
      • 1970-01-01
      • 2021-12-26
      • 2020-08-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多