我经常在 Scala 文献中遇到“abstract over”这个短语,但我不明白其意图。 For example,马丁·奥德斯基写道
您可以将方法(或“函数”)作为参数传递,也可以对它们进行抽象。您可以将类型指定为参数,也可以对它们进行抽象。
再举一个例子,在 "Deprecating the Observer Pattern" 论文中,
我们的事件流是一等值的结果是我们可以对它们进行抽象。
我已经读过一阶泛型“抽象于类型”,而 monads“抽象于类型构造函数”。我们还在 Cake Pattern paper 中看到了类似的短语。引用许多这样的例子之一:
抽象类型成员提供了对具体类型的组件进行抽象的灵活方式。
甚至相关的堆栈溢出问题也使用这个术语。 "can't existentially abstract over parameterized type..."
那么......“抽象”实际上是什么意思?
在代数中,就像在日常概念形成中一样,抽象是通过按一些基本特征对事物进行分组并省略其特定的其他特征而形成的。抽象统一在一个表示相似性的符号或单词下。我们说我们对差异进行抽象,但这实际上意味着我们通过相似性进行整合。
例如,考虑一个接受数字 1
、2
和 3
之和的程序:
val sumOfOneTwoThree = 1 + 2 + 3
这个程序不是很有趣,因为它不是很抽象。通过在单个符号 ns
下整合所有数字列表,我们可以抽象我们正在求和的数字:
def sumOf(ns: List[Int]) = ns.foldLeft(0)(_ + _)
而且我们也不特别关心它是否是一个列表。 List 是一个特定的类型构造函数(接受一个类型并返回一个类型),但我们可以通过指定我们想要的基本特征(它可以折叠)来抽象类型构造函数:
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
}
def sumOf[F[_]](ns: F[Int])(implicit ff: Foldable[F]) =
ff.foldl(ns, 0, (x: Int, y: Int) => x + y)
我们可以为 List
和任何其他我们可以折叠的东西提供隐式 Foldable
实例。
implicit val listFoldable = new Foldable[List] {
def foldl[A, B](as: List[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}
implicit val setFoldable = new Foldable[Set] {
def foldl[A, B](as: Set[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}
val sumOfOneTwoThree = sumOf(List(1,2,3))
更重要的是,我们可以对操作和操作数的类型进行抽象:
trait Monoid[M] {
def zero: M
def add(m1: M, m2: M): M
}
trait Foldable[F[_]] {
def foldl[A, B](as: F[A], z: B, f: (B, A) => B): B
def foldMap[A, B](as: F[A], f: A => B)(implicit m: Monoid[B]): B =
foldl(as, m.zero, (b: B, a: A) => m.add(b, f(a)))
}
def mapReduce[F[_], A, B](as: F[A], f: A => B)
(implicit ff: Foldable[F], m: Monoid[B]) =
ff.foldMap(as, f)
现在我们有了一些非常普遍的东西。方法 mapReduce
将折叠任何 F[A]
,因为我们可以证明 F
是可折叠的并且 A
是一个幺半群或可以映射成一个。例如:
case class Sum(value: Int)
case class Product(value: Int)
implicit val sumMonoid = new Monoid[Sum] {
def zero = Sum(0)
def add(a: Sum, b: Sum) = Sum(a.value + b.value)
}
implicit val productMonoid = new Monoid[Product] {
def zero = Product(1)
def add(a: Product, b: Product) = Product(a.value * b.value)
}
val sumOf123 = mapReduce(List(1,2,3), Sum)
val productOf456 = mapReduce(Set(4,5,6), Product)
我们已经抽象了幺半群和可折叠。
大致而言,能够“抽象”某物意味着您可以为其创建一个参数,而不是直接使用该东西,或者以其他方式“匿名”使用它。
Scala 允许您对类型进行抽象,方法是允许类、方法和值具有类型参数,并允许值具有抽象(或匿名)类型。
Scala 允许您通过允许方法具有函数参数来抽象操作。
Scala 允许您通过允许在结构上定义类型来对特性进行抽象。
Scala 允许您通过允许高阶类型参数来抽象类型参数。
Scala 允许您通过创建提取器来抽象数据访问模式。
Scala 允许您通过允许隐式转换作为参数来抽象“可以用作其他东西的东西”。 Haskell 对类型类做类似的事情。
Scala(还)不允许您对类进行抽象。您不能将类传递给某物,然后使用该类创建新对象。其他语言确实允许对类进行抽象。
(“Monads abstract over type constructors”仅在非常严格的情况下才成立。在你有“啊哈!我理解 monads !!”时刻之前,不要挂断它。)
抽象计算的某些方面的能力基本上是允许代码重用的能力,并能够创建功能库。与更主流的语言相比,Scala 允许抽象更多种类的东西,并且 Scala 中的库可以相应地更强大。
Manifest
,甚至是 Class
,并使用反射来实例化该类的新对象。
抽象是一种概括。
http://en.wikipedia.org/wiki/Abstraction
不仅在 Scala 中,而且在许多语言中,都需要有这样的机制来降低复杂性(或者至少创建一个层次结构,将信息划分为更容易理解的部分)。
类是对简单数据类型的抽象。它有点像基本类型,但实际上概括了它们。因此,类不仅仅是一种简单的数据类型,而且与它有许多共同点。
当他说“抽象化”时,他指的是你概括的过程。因此,如果您将方法抽象为参数,那么您就是在概括执行此操作的过程。例如,您可以创建某种通用的方法来处理它,而不是将方法传递给函数(例如根本不传递方法,而是建立一个特殊的系统来处理它)。
在这种情况下,他特别指的是抽象问题并为问题创建类似 oop 的解决方案的过程。 C 几乎没有抽象能力(你可以做到,但它很快就会变得一团糟,而且语言不直接支持它)。如果你用 C++ 编写它,你可以使用 oop 概念来降低问题的复杂性(嗯,它是相同的复杂性,但概念化通常更容易(至少一旦你学会了用抽象来思考))。
例如,如果我需要一个类似于 int 的特殊数据类型,但假设是受限的,我可以通过创建一个可以像 int 一样使用但具有我需要的属性的新类型来抽象它。我用来做这种事情的过程将被称为“抽象”。
这是我狭隘的表演和讲述的解释。它是不言自明的并且在 REPL 中运行。
class Parameterized[T] { // type as a parameter
def call(func: (Int) => Int) = func(1) // function as a parameter
def use(l: Long) { println(l) } // value as a parameter
}
val p = new Parameterized[String] // pass type String as a parameter
p.call((i:Int) => i + 1) // pass function increment as a parameter
p.use(1L) // pass value 1L as a parameter
abstract class Abstracted {
type T // abstract over a type
def call(i: Int): Int // abstract over a function
val l: Long // abstract over value
def use() { println(l) }
}
class Concrete extends Abstracted {
type T = String // specialize type as String
def call(i:Int): Int = i + 1 // specialize function as increment function
val l = 1L // specialize value as 1L
}
val a: Abstracted = new Concrete
a.call(1)
a.use()
其他答案已经很好地了解了存在哪些抽象。让我们一一回顾引用,并提供一个例子:
您可以将方法(或“函数”)作为参数传递,也可以对它们进行抽象。您可以将类型指定为参数,也可以对它们进行抽象。
将函数作为参数传递:List(1,-2,3).map(math.abs(x))
显然 abs
在这里作为参数传递。 map
本身抽象了一个函数,该函数对每个列表元素执行特定的特殊操作。 val list = List[String]()
指定类型参数(字符串)。您可以编写一个使用抽象类型成员的集合类型:val buffer = Buffer{ type Elem=String }
。一个区别是您必须写 def f(lis:List[String])...
但 def f(buffer:Buffer)...
,因此元素类型在第二种方法中是一种“隐藏”。
我们的事件流是一等值的结果是我们可以对它们进行抽象。
在 Swing 中,一个事件突然“发生”,你必须在此时此地处理它。事件流允许您以更具声明性的方式完成所有管道连接。例如,当您想在 Swing 中更改负责的侦听器时,您必须取消注册旧的并注册新的,并了解所有血淋淋的细节(例如线程问题)。使用事件流,事件的来源成为您可以简单地传递的东西,使其与字节或字符流没有太大区别,因此是一个更“抽象”的概念。
抽象类型成员提供了对具体类型的组件进行抽象的灵活方式。
上面的 Buffer 类已经是一个例子。
上面的答案提供了一个很好的解释,但是用一句话来总结它,我会说:
对某事进行抽象与在不相关的地方忽略它是一样的。
Set
或其他可折叠类型,那将会很有趣。带有String
和连接的示例也很酷。