我正在阅读 Scala之旅:抽象类型。何时更适合使用抽象类型?
例如,
abstract class Buffer {
type T
val element: T
}
相比于泛型,例如,
abstract class Buffer[T] {
val element: T
}
我正在阅读 Scala之旅:抽象类型。何时更适合使用抽象类型?
例如,
abstract class Buffer {
type T
val element: T
}
abstract class Buffer[T] {
val element: T
}
您在这个问题上有一个很好的观点:
Scala类型系统的目的
与Martin Odersky的谈话,第三部分
由Bill Venners和Frank Sommers(2009年5月18日)
更新(2009年10月):以下内容已在Bill Venners的新文章中得到说明:
抽象类型成员与Scala中的通用类型参数(请参见结尾摘要)
以下是有关第一次采访的相关摘录,时间为2009年5月(强调是我的)
一直以来,抽象有两个概念:
在Java中,您也有这两种方式,但这取决于您要抽象的内容。
在Java中,您有抽象方法,但无法将方法作为参数传递。
您没有抽象字段,但可以将值作为参数传递。
同样,您没有抽象类型成员,但可以将类型指定为参数。
因此,在Java中,您也拥有所有三者,但区分了哪种抽象原则适用于哪种事物。您可以认为这种区分是相当武断的。
我们决定对所有三种成员使用相同的构造原则。
因此,您可以有抽象字段以及值参数。
您可以将方法(或“函数”)作为参数传递,或者进行抽象处理。
您可以将类型指定为参数,或进行抽象处理。
从概念上讲,我们可以将一个模型转换为另一个模型。至少从原则上讲,我们可以将每种类型的参数化表达为面向对象的抽象形式。因此,您可以认为Scala是一种更正交和完整的语言。
thatismatt在评论中问道:
你认为以下总结是否公正:
- 抽象类型用于“拥有”或“使用”关系(例如,
Cow eats Grass
)- 而泛型通常是“of”关系(例如,
List of Ints
)
我不确定使用抽象类型或泛型之间的关系是否有那么大的差别。 不同之处在于:
注意:家族多态已被提议作为面向对象语言的解决方案,以支持可重用但类型安全的相互递归类。 家族多态的一个关键思想是家族的概念,用于将相互递归的类分组。抽象类型成员提供了一种灵活的方式来抽象组件的具体类型。抽象类型可以隐藏组件内部信息,类似于它们在SML签名中的使用。在面向对象的框架中,由于类可以通过继承进行扩展,因此它们也可以用作一种灵活的参数化手段(通常称为家族多态性,请参见本网络日志条目以及Eric Ernst撰写的论文)。
abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}
{ type O = T }
组成。O
等于equals T
。<
方法保证可以应用于类型为T的接收器和参数。Scala中的抽象类型成员与泛型类型参数(Bill Venners)
(强调是我的)
到目前为止,我对抽象类型成员的观察是,当:
- 你想要让人们通过特质混合定义这些类型时。
- 你认为在定义时显式提及类型成员名称将有助于代码可读性时。
抽象类型成员通常比泛型类型参数更好。
例如:
如果您想将三个不同的fixture对象传递到测试中,您可以这样做,但是您需要指定三种类型,每个参数一个。因此,如果我采用了类型参数方法,您的套件类可能会变成这样:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
如果没有查阅,他们不会知道指定为StringBuilder的类型参数的名称。而在抽象类型成员方法中,类型参数的名称就在代码中:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
StringBuilder
是“装置参数”类型。他们仍然需要弄清楚“装置参数”的含义,但至少可以在不查看文档的情况下获取类型的名称。当我在阅读Scala相关的资料时,也曾有过同样的问题。
使用泛型的优点在于你可以创建一系列类型的家族。没有人需要对Buffer
进行子类化——他们只需使用Buffer[Any]
、Buffer[String]
等即可。
如果你使用抽象类型,那么人们就必须创建一个子类。人们需要像AnyBuffer
、StringBuffer
这样的类。
你需要决定哪种方式更适合你的特定需求。
Buffer { type T <: String }
或 Buffer { type T = String }
。请注意,我的翻译可能不是逐字逐句的,但它保留了原始意思并尽可能简洁易懂。 - Eduardo Pareja Tobes您可以结合类型参数来使用抽象类型,以建立自定义模板。
假设您需要使用三个相连的特质来建立一个模式:
trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
按照泛型参数中提到的AA、BB、CC本身分别来解释。
以下是可能的代码示例:
trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
由于类型参数绑定,这种简单的方式不起作用。您需要将其协变才能正确继承。
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
这个示例可以编译,但它对方差规则有强制要求,并且在某些情况下无法使用。
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
def forth(x:B):C
def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
def forth(x:C):A
def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
def forth(x:A):B
def back(x:B):A
}
编译器会抛出许多变异检查错误。//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
type A <: AA[O]
type B <: BB[O]
type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
type A = O#A
type B = O#B
type C = O#C
def left(l:B):C
def right(r:C):B = r.left(this)
def join(l:B, r:C):A
def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
type A = O#A
type B = O#B
type C = O#C
def left(l:C):A
def right(r:A):C = r.left(this)
def join(l:C, r:A):B
def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
type A = O#A
type B = O#B
type C = O#C
def left(l:A):B
def right(r:B):A = r.left(this)
def join(l:A, r:B):C
def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}
现在我们可以为描述的模式编写具体表示,定义所有类中的左和加入方法,并免费获得正确和双重效果。
class ReprO extends OO[ReprO] {
override type A = ReprA
override type B = ReprB
override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
override def left(l:B):C = ReprC(data - l.data)
override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
override def left(l:C):A = ReprA(data - l.data)
override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
override def left(l:A):B = ReprB(data - l.data)
override def join(l:A, r:B):C = ReprC(l.data + r.data)
}
因此,抽象类型和类型参数都用于创建抽象,它们各有优缺点。抽象类型更具体,并能够描述任何类型结构,但较为冗长且需要明确指定。类型参数可以立即创建一堆类型,但会增加继承和类型边界的额外担忧。
它们相互协作,可以结合使用,创建无法仅通过其中一个表示的复杂抽象。
def forth[B1 >: B](x: B1): C
,def back[C1 >: C](x: C1): B
等。 - Dmytro MitinStephen Compall. 类型成员几乎是类型参数 https://typelevel.org/blog/2015/07/13/type-members-parameters.html
Jon Pretty @propensive. 类型成员 vs 类型参数 - NE Scala 2016 https://www.youtube.com/watch?v=R8GksuRw3VI
类型参数可以变成类型成员
trait A[_T] {
type T = _T
}
类型成员可以作为类型参数
trait A { type T }
object A {
type Aux[_T] = A { type T = _T }
}
// using A.Aux[T] instead of A[T]
但是:
{ self: T => ...
中使用。类型参数不能直接在实例上调用。类型成员可以被视为命名的类型参数。trait A[T]
val a: A[Int] = ???
type X = ?? // what is T of a?
trait A { type T }
val a: A { type T = Int } = ???
type X = a.T
trait A[_T] { type T = _T }
val a: A[Int] = ???
type X = a.T
trait A[+T] // definition
trait A[-T] // definition
trait A[T]
type X[+T] = A[_ <: T] // call
type Y[-T] = A[_ >: T] // call
trait A { type T }
type X[+_T] = A { type T <: _T } // call
type Y[-_T] = A { type T >: _T } // call
在Scala3中,如果将泛型类型参数映射到依赖类型,协变和逆变修饰符如何映射?
trait MyTrait { type A; type B; type C }
,您可以指定某些类型而不指定其他类型。但是对于trait MyTrait[A, B, C]
,您只能指定所有类型或不指定任何类型。因此,类型参数更像输入(要指定的)而类型成员更像输出(要推断的)。为什么我们需要为某些类型计算的输出指定精制类型(或其等效Aux)?
Nothing
)。但是类型成员可以保持抽象。"def apply[T](c:T)"和"type T;def apply(c:T)"有什么不同?
为什么Scala在未指定类型参数时会推断底部类型? (答案)
trait A[T] { type S }
中,类型S
有时可以在功能上依赖于T
,而在trait A[T, S]
中,类型T
、S
是任意的,在Haskell中为class A t s | t -> s
或class A t where type S t
与class A t s
)。https://github.com/lampepfl/dotty/issues/17212
https://github.com/scala/bug/issues/12767
https://github.com/lampepfl/dotty/issues/17235
https://dotty.epfl.ch/docs/internals/higher-kinded-v2.html
https://contributors.scala-lang.org/t/scala-3-type-parameters-and-type-members/3472
我认为这里没有太大的区别。类型抽象成员可以被视为存在类型,类似于其他一些函数式语言中的记录类型。
例如,我们有:
class ListT {
type T
...
}
并且
class List[T] {...}
那么ListT
就和List[_]
一样了。
类型成员的便利之处在于我们可以使用类而不需要显式具体类型,
避免过多的类型参数。