我可以在 Predef 的 API 文档中看到它们是通用函数类型 (From) => 的子类。到,但仅此而已。嗯什么?也许某处有文档,但搜索引擎不处理像“<:<”这样的“名称”很好,所以一直找不到。
后续问题:我什么时候应该使用这些时髦的符号/类,为什么?
typeclass
是否执行这些运算符的工作?示例:compare :: Ord a => a -> a -> Ordering
?我试图从它的 Haskell 对应部分来理解这个 Scala 概念。
这些称为广义类型约束。它们允许您从类型参数化的类或特征中进一步约束其类型参数之一。这是一个例子:
case class Foo[A](a:A) { // 'A' can be substituted with any type
// getStringLength can only be used if this is a Foo[String]
def getStringLength(implicit evidence: A =:= String) = a.length
}
隐式参数 evidence
由编译器提供,当且仅当 A
为 String
。您可以将其视为 A
是 String
的证明 - 参数本身并不重要,只需知道它存在即可。 [编辑:嗯,从技术上讲,它实际上很重要,因为它表示从 A
到 String
的隐式转换,这使您可以调用 a.length
而不会让编译器对您大喊大叫]
现在我可以像这样使用它:
scala> Foo("blah").getStringLength
res6: Int = 4
但是,如果我尝试将它与包含 String
以外的内容的 Foo
一起使用:
scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]
您可以将该错误理解为“无法找到 Int == String 的证据”......应该是这样! getStringLength
对 A
的类型施加了更多的限制,而不是 Foo
的一般要求;即,您只能在 Foo[String]
上调用 getStringLength
。这个约束是在编译时强制执行的,这很酷!
<:<
和 <%<
的工作方式类似,但略有不同:
A =:= B 意味着 A 必须是 B
A <:< B 表示 A 必须是 B 的子类型(类似于简单类型约束 <:)
A <%< B 意味着 A 必须作为 B 可见,可能通过隐式转换(类似于简单类型约束 <%)
@retronym 的 This snippet 很好地解释了这种事情过去是如何完成的,以及通用类型约束现在如何使它变得更容易。
附录
为了回答您的后续问题,诚然,我给出的示例非常做作,显然没有用。但想象一下,使用它来定义类似 List.sumInts
的方法,该方法将整数列表相加。您不想允许在任何旧的 List
上调用此方法,而只是在 List[Int]
上调用。但是 List
类型的构造函数不能如此受限制;您仍然希望能够拥有字符串、foos、bars 和诸如此类的列表。因此,通过在 sumInts
上放置通用类型约束,您可以确保 仅该方法 具有只能在 List[Int]
上使用的附加约束。本质上,您正在为某些类型的列表编写特殊情况代码。
不是一个完整的答案(其他人已经回答了这个问题),我只想注意以下几点,这可能有助于更好地理解语法:您通常使用这些“运算符”的方式,例如在 pelotom 的示例中:
def getStringLength(implicit evidence: A =:= String)
使用 Scala 的替代方案 infix syntax for type operators。
因此,A =:= String
与 =:=[A, String]
相同(并且 =:=
只是一个具有花哨名称的类或特征)。请注意,此语法也适用于“常规”类,例如您可以编写:
val a: Tuple2[Int, String] = (1, "one")
像这样:
val a: Int Tuple2 String = (1, "one")
它类似于方法调用的两种语法,即带有 .
和 ()
的“普通”语法以及运算符语法。
makes use of Scala's alternative infix syntax for type operators.
完全错过了这个解释,否则整个事情就没有意义
阅读其他答案以了解这些构造是什么。这是您应该使用它们的时候。当您只需要为特定类型约束方法时,您可以使用它们。
这是一个例子。假设您要定义一个同质的 Pair,如下所示:
class Pair[T](val first: T, val second: T)
现在您要添加一个方法 smaller
,如下所示:
def smaller = if (first < second) first else second
仅在订购 T
时才有效。您可以限制整个班级:
class Pair[T <: Ordered[T]](val first: T, val second: T)
但这似乎是一种耻辱——当 T
没有被订购时,这个类可能会有用处。使用类型约束,您仍然可以定义 smaller
方法:
def smaller(implicit ev: T <:< Ordered[T]) = if (first < second) first else second
可以实例化,例如,Pair[File]
,只要您不调用 smaller
。
在 Option
的情况下,实现者想要一个 orNull
方法,即使它对 Option[Int]
没有意义。通过使用类型约束,一切都很好。您可以在 Option[String]
上使用 orNull
,并且您可以形成一个 Option[Int]
并使用它,只要您不对它调用 orNull
。如果您尝试 Some(42).orNull
,您会收到迷人的信息
error: Cannot prove that Null <:< Int
<:<
的用例,并且我认为 Ordered
示例不再那么引人注目,因为现在您宁愿使用 Ordering
类型类而不是Ordered
特征。类似于:def smaller(implicit ord: Ordering[T]) = if (ord.lt(first, second)) first else second
。
这取决于它们在哪里使用。大多数情况下,在声明隐式参数类型时使用它们是类。在极少数情况下,它们也可能是对象。最后,它们可以是 Manifest
对象的运算符。在前两种情况下,它们是在 scala.Predef
中定义的,尽管没有特别好的文档。
它们旨在提供一种方法来测试类之间的关系,就像 <:
和 <%
一样,在后者无法使用的情况下。
至于“我什么时候应该使用它们?”的问题,答案是你不应该,除非你知道你应该。 :-) EDIT:好的,好的,这里有一些来自图书馆的例子。在 Either
,您有:
/**
* Joins an <code>Either</code> through <code>Right</code>.
*/
def joinRight[A1 >: A, B1 >: B, C](implicit ev: B1 <:< Either[A1, C]): Either[A1, C] = this match {
case Left(a) => Left(a)
case Right(b) => b
}
/**
* Joins an <code>Either</code> through <code>Left</code>.
*/
def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]): Either[C, B1] = this match {
case Left(a) => a
case Right(b) => Right(b)
}
在 Option
,您有:
def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse null
您会在这些集合中找到一些其他示例。
:-)
是其中之一吗?我同意你对“我什么时候应该使用它们?”的回答。适用于很多事情。
Manifest
上也有同名的方法,你没有提到。Manifest
上的方法只有<:<
和>:>
……因为 OP 准确地提到了 3 种广义类型约束,我假设这就是他感兴趣的。class =:=[From, To] extends From => To
,这意味着From =:= To
类型的隐式值实际上是从From
到To
的隐式转换。因此,通过接受A =:= String
类型的隐式参数,您是在说A
可以隐式转换为String
。如果您更改了顺序并使隐式参数的类型为String =:= A
,它将不起作用,因为这将是从String
到A
的隐式转换。From =:= To
的隐式值意味着您有一个隐式转换From => To
,但其含义不会向后运行;有一个隐式转换A => B
不 暗示你有一个A =:= B
的实例。=:=
是scala.Predef
中定义的密封抽象类,只有一个公开暴露的实例,它是隐式的,属于A =:= A
类型。因此,您可以保证A =:= B
类型的隐式值见证了A
和B
相等的事实。