我知道抽象是指把一些更具体的东西变得更抽象。那东西可能是数据结构或过程。例如:
- 数据抽象:矩形是正方形的抽象。它专注于正方形有两对相对边的事实,而忽略了正方形的相邻边相等的事实。
- 过程抽象:高阶函数
map
是过程的抽象,它对值列表执行一些操作以生成全新的值列表。它专注于过程循环遍历列表的每个项目以生成新列表的事实,并忽略对列表的每个项目执行的实际操作。
所以我的问题是:抽象与泛化有何不同?我正在寻找主要与函数式编程相关的答案。但是,如果在面向对象编程中有相似之处,那么我也想了解这些。
我知道抽象是指把一些更具体的东西变得更抽象。那东西可能是数据结构或过程。例如:
map
是过程的抽象,它对值列表执行一些操作以生成全新的值列表。它专注于过程循环遍历列表的每个项目以生成新列表的事实,并忽略对列表的每个项目执行的实际操作。所以我的问题是:抽象与泛化有何不同?我正在寻找主要与函数式编程相关的答案。但是,如果在面向对象编程中有相似之处,那么我也想了解这些。
确实是一个非常有趣的问题。我发现这篇关于该主题的文章,简明扼要地指出:
抽象通过隐藏不相关的细节来降低复杂性,而泛化通过用单个构造替换执行相似功能的多个实体来降低复杂性。
让我们以一个为图书馆管理书籍的系统为例。一本书有很多属性(页数、重量、字体大小、封面……),但为了我们图书馆的目的,我们可能只需要
Book(title, ISBN, borrowed)
我们只是从我们图书馆中的真实书籍中抽象出来,并且只在我们的应用程序的上下文中获取我们感兴趣的属性。
另一方面,泛化并不试图删除细节,而是使功能适用于更广泛(更通用)的项目范围。通用容器是这种心态的一个很好的例子:你不想编写 , 等的实现StringList
,IntList
这就是为什么你宁愿编写一个适用于所有类型的通用List[T]
List (就像在 Scala 中一样)。请注意,您没有抽象列表,因为您没有删除任何细节或操作,您只是使它们普遍适用于您的所有类型。
@dtldarek 的回答真是一个很好的例证!基于它,这里有一些代码可能会提供进一步的说明。
还记得Book
我提到的吗?当然,图书馆中还有其他可以借用的东西(我将调用所有这些对象的集合,Borrowable
即使这可能甚至不是一个词:D):
所有这些项目都将在我们的数据库和业务逻辑中具有抽象表示,可能类似于我们的Book
. 此外,我们可以定义一个所有Borrowable
s 共有的特征:
trait Borrowable {
def itemId:Long
}
然后我们可以编写适用于所有s 的通用Borrowable
逻辑(此时我们不在乎它是一本书还是一本杂志):
object Library {
def lend(b:Borrowable, c:Customer):Receipt = ...
[...]
}
总结一下:我们在我们的数据库中存储了所有书籍、杂志和 DVD 的抽象表示,因为准确的表示既不可行也没有必要。然后我们继续说
客户是否借用了一本书、一本杂志或一张 DVD 并不重要。它总是相同的过程。
因此,我们通过将可以借用的所有东西定义为s来概括借用项目的操作。Borrowable
目的:
抽象:
概括:
Haskell 中的示例:
使用优先级队列实现选择排序,具有三个不同的接口:
{-# LANGUAGE RankNTypes #-}
module Main where
import qualified Data.List as List
import qualified Data.Set as Set
{- TYPES: -}
-- PQ new push pop
-- by intention there is no build-in way to tell if the queue is empty
data PriorityQueue q t = PQ (q t) (t -> q t -> q t) (q t -> (t, q t))
-- there is a concrete way for a particular queue, e.g. List.null
type ListPriorityQueue t = PriorityQueue [] t
-- but there is no method in the abstract setting
newtype AbstractPriorityQueue q = APQ (forall t. Ord t => PriorityQueue q t)
{- SOLUTIONS: -}
-- the basic version
list_selection_sort :: ListPriorityQueue t -> [t] -> [t]
list_selection_sort (PQ new push pop) list = List.unfoldr mypop (List.foldr push new list)
where
mypop [] = Nothing -- this is possible because we know that the queue is represented by a list
mypop ls = Just (pop ls)
-- here we abstract the queue, so we need to keep the queue size ourselves
abstract_selection_sort :: Ord t => AbstractPriorityQueue q -> [t] -> [t]
abstract_selection_sort (APQ (PQ new push pop)) list = List.unfoldr mypop (List.foldr mypush (0,new) list)
where
mypush t (n, q) = (n+1, push t q)
mypop (0, q) = Nothing
mypop (n, q) = let (t, q') = pop q in Just (t, (n-1, q'))
-- here we generalize the first solution to all the queues that allow checking if the queue is empty
class EmptyCheckable q where
is_empty :: q -> Bool
generalized_selection_sort :: EmptyCheckable (q t) => PriorityQueue q t -> [t] -> [t]
generalized_selection_sort (PQ new push pop) list = List.unfoldr mypop (List.foldr push new list)
where
mypop q | is_empty q = Nothing
mypop q | otherwise = Just (pop q)
{- EXAMPLES: -}
-- priority queue based on lists
priority_queue_1 :: Ord t => ListPriorityQueue t
priority_queue_1 = PQ [] List.insert (\ls -> (head ls, tail ls))
instance EmptyCheckable [t] where
is_empty = List.null
-- priority queue based on sets
priority_queue_2 :: Ord t => PriorityQueue Set.Set t
priority_queue_2 = PQ Set.empty Set.insert Set.deleteFindMin
instance EmptyCheckable (Set.Set t) where
is_empty = Set.null
-- an arbitrary type and a queue specially designed for it
data ABC = A | B | C deriving (Eq, Ord, Show)
-- priority queue based on counting
data PQ3 t = PQ3 Integer Integer Integer
priority_queue_3 :: PriorityQueue PQ3 ABC
priority_queue_3 = PQ new push pop
where
new = (PQ3 0 0 0)
push A (PQ3 a b c) = (PQ3 (a+1) b c)
push B (PQ3 a b c) = (PQ3 a (b+1) c)
push C (PQ3 a b c) = (PQ3 a b (c+1))
pop (PQ3 0 0 0) = undefined
pop (PQ3 0 0 c) = (C, (PQ3 0 0 (c-1)))
pop (PQ3 0 b c) = (B, (PQ3 0 (b-1) c))
pop (PQ3 a b c) = (A, (PQ3 (a-1) b c))
instance EmptyCheckable (PQ3 t) where
is_empty (PQ3 0 0 0) = True
is_empty _ = False
{- MAIN: -}
main :: IO ()
main = do
print $ list_selection_sort priority_queue_1 [2, 3, 1]
-- print $ list_selection_sort priority_queue_2 [2, 3, 1] -- fail
-- print $ list_selection_sort priority_queue_3 [B, C, A] -- fail
print $ abstract_selection_sort (APQ priority_queue_1) [B, C, A] -- APQ hides the queue
print $ abstract_selection_sort (APQ priority_queue_2) [B, C, A] -- behind the layer of abstraction
-- print $ abstract_selection_sort (APQ priority_queue_3) [B, C, A] -- fail
print $ generalized_selection_sort priority_queue_1 [2, 3, 1]
print $ generalized_selection_sort priority_queue_2 [B, C, A]
print $ generalized_selection_sort priority_queue_3 [B, C, A]-- power of generalization
-- fail
-- print $ let f q = (list_selection_sort q [2,3,1], list_selection_sort q [B,C,A])
-- in f priority_queue_1
-- power of abstraction (rank-n-types actually, but never mind)
print $ let f q = (abstract_selection_sort q [2,3,1], abstract_selection_sort q [B,C,A])
in f (APQ priority_queue_1)
-- fail
-- print $ let f q = (generalized_selection_sort q [2,3,1], generalized_selection_sort q [B,C,A])
-- in f priority_queue_1
该代码也可以通过pastebin获得。
值得注意的是存在类型。正如@lukstafi 已经指出的那样,抽象类似于存在量词,而泛化类似于全称量词。观察到 ∀xP(x) 隐含 ∃xP(x)(在非空宇宙中)这一事实与很少有没有抽象的概括(即使是类似 c++ 的重载函数形式)之间存在非平凡的联系某种意义上的一种抽象)。
致谢:Solo 的门户蛋糕。djttwo的甜品桌。该符号是来自material.io的蛋糕图标。
我将使用一些例子来描述泛化和抽象,我将参考这篇文章。
据我所知,编程领域中抽象和泛化的定义没有官方来源(在我看来,维基百科可能是最接近官方定义的),所以我改用了一篇我认为的文章可信的。
概括
文章指出:
“OOP 中泛化的概念意味着一个对象封装了一类对象的公共状态和行为。”
因此,例如,如果您将泛化应用于形状,则所有类型的形状的共同属性是面积和周长。
因此,广义形状(例如 Shape)和它的特化(例如圆形)可以用如下类表示(请注意,此图像取自上述文章)
同样,如果您在喷气式飞机领域工作,则可以将 Jet 作为概括,它具有翼展属性。喷气式飞机的专业化可能是战斗机,它将继承翼展属性并具有其自己的战斗机独有的属性,例如 NumberOfMissiles。
抽象
文章将抽象定义为:
“识别具有系统变化的共同模式的过程;抽象代表共同模式并提供指定使用哪个变化的方法”(理查德加布里埃尔)“
在编程领域:
抽象类是允许继承但永远不能实例化的父类。
因此,在上面泛化部分给出的示例中,Shape 是抽象的:
在现实世界中,您永远不会计算通用形状的面积或周长,您必须知道您拥有什么样的几何形状,因为每种形状(例如正方形、圆形、矩形等)都有自己的面积和周长公式。
然而,除了抽象之外,形状也是一种概括(因为它“封装了一类对象的常见状态和行为”,在这种情况下,对象是形状)。
回到我给出的关于喷气机和战斗机的例子,喷气机不是抽象的,因为喷气机的具体实例是可行的,因为它可以存在于现实世界中,不像形状,即在现实世界中你不能保持你的形状持有一个形状的实例,例如一个立方体。所以在飞机的例子中,喷气机不是抽象的,它是一种概括,因为它可能有一个喷气机的“具体”实例。
未解决可信/官方来源:Scala 中的示例
有“抽象”
trait AbstractContainer[E] { val value: E }
object StringContainer extends AbstractContainer[String] {
val value: String = "Unflexible"
}
class IntContainer(val value: Int = 6) extends AbstractContainer[Int]
val stringContainer = new AbstractContainer[String] {
val value = "Any string"
}
和“泛化”
def specialized(c: StringContainer.type) =
println("It's a StringContainer: " + c.value)
def slightlyGeneralized(s: AbstractContainer[String]) =
println("It's a String container: " + s.value)
import scala.reflect.{ classTag, ClassTag }
def generalized[E: ClassTag](a: AbstractContainer[E]) =
println(s"It's a ${classTag[E].toString()} container: ${a.value}")
import scala.language.reflectiveCalls
def evenMoreGeneral(d: { def detail: Any }) =
println("It's something detailed: " + d.detail)
执行
specialized(StringContainer)
slightlyGeneralized(stringContainer)
generalized(new IntContainer(12))
evenMoreGeneral(new { val detail = 3.141 })
导致
It's a StringContainer: Unflexible
It's a String container: Any string
It's a Int container: 12
It's something detailed: 3.141
抽象
抽象是指定框架并隐藏实现级别信息。具体性将建立在抽象之上。它为您提供了在实施细节时可以遵循的蓝图。抽象通过隐藏低级细节来降低复杂性。
示例:汽车的线框模型。
概括
泛化使用从专业化到泛化类的“is-a”关系。从专业到通用类都使用通用结构和行为。在更广泛的层面上,您可以将其理解为继承。为什么我使用继承这个词是,你可以很好地关联这个词。泛化也称为“Is-a”关系。
示例:假设存在一个名为 Person 的类。学生是一个人。教师是一个人。因此这里的学生与人的关系,同样教师与人的关系是泛化的。
我想为尽可能多的观众提供答案,因此我使用网络通用语言 Javascript。
让我们从一段普通的命令式代码开始:
// some data
const xs = [1,2,3];
// ugly global state
const acc = [];
// apply the algorithm to the data
for (let i = 0; i < xs.length; i++) {
acc[i] = xs[i] * xs[i];
}
console.log(acc); // yields [1, 4, 9]
在下一步中,我将介绍编程中最重要的抽象——函数。函数对表达式进行抽象:
// API
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x]; // weird square function to keep the example simple
// some data
const xs = [1,2,3];
// applying
console.log(
foldr(x => acc => concat(sqr_(x)) (acc)) ([]) (xs) // [1, 4, 9]
)
正如你所看到的,很多实现细节都被抽象掉了。抽象意味着对细节的压制。
另一个抽象步骤...
// API
const comp = (f, g) => x => f(g(x));
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
// some data
const xs = [1,2,3];
// applying
console.log(
foldr(comp(concat, sqr_)) ([]) (xs) // [1, 4, 9]
);
还有一个:
// API
const concatMap = f => foldr(comp(concat, f)) ([]);
const comp = (f, g) => x => f(g(x));
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
// some data
const xs = [1,2,3];
// applying
console.log(
concatMap(sqr_) (xs) // [1, 4, 9]
);
基本原则现在应该很清楚了。不过我还是不满意concatMap
,因为它只适用于Array
s. 我希望它适用于所有可折叠的数据类型:
// API
const concatMap = foldr => f => foldr(comp(concat, f)) ([]);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
const comp = (f, g) => x => f(g(x));
// Array
const xs = [1, 2, 3];
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
// Option (another foldable data type)
const None = r => f => r;
const Some = x => r => f => f(x);
const foldOption = f => acc => tx => tx(acc) (x => f(x) (acc));
// applying
console.log(
concatMap(foldr) (sqr_) (xs), // [1, 4, 9]
concatMap(foldOption) (sqr_) (Some(3)), // [9]
concatMap(foldOption) (sqr_) (None) // []
);
我扩大了应用范围concatMap
以涵盖更大的数据类型领域,即所有可折叠数据类型。泛化强调不同类型(或者更确切地说是对象、实体)之间的共性。
我通过字典传递(concatMap
在我的示例中的附加参数)实现了这一点。现在在整个代码中传递这些类型的字典有点烦人。因此,Haskell 人员将类型类引入到,...嗯,抽象类型字典:
concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\x -> [x * x]) ([1,2,3]) -- yields [1, 4, 9]
concatMap (\x -> [x * x]) (Just 3) -- yields [9]
concatMap (\x -> [x * x]) (Nothing) -- yields []
因此,Haskell 的泛型concatMap
受益于抽象和泛化。
让我以最简单的方式解释。
“所有漂亮的女孩都是女性。” 是一个抽象。
“所有漂亮的女孩都会化妆。” 是一个概括。
抽象通常是通过消除不必要的细节来降低复杂性。例如,OOP 中的抽象类是一个父类,它包含其子类的共同特征,但没有指定确切的功能。
泛化不一定需要避免细节,而是需要有一些机制来允许将相同的函数应用于不同的参数。例如,函数式编程语言中的多态类型让您不必担心参数,而是专注于函数的操作。同样,在 java 中,您可以拥有泛型类型,它是所有类型的“保护伞”,而功能相同。