【问题标题】:What's the difference between abstraction and generalization?抽象和泛化有什么区别?
【发布时间】:2013-10-17 23:27:45
【问题描述】:

我知道抽象是指将一些更具体的东西变得更抽象。那东西可能是数据结构或过程。例如:

  1. 数据抽象:矩形是正方形的抽象。它专注于正方形有两对相对边这一事实,而忽略了正方形的相邻边相等这一事实。
  2. 过程抽象: 高阶函数map 是过程的抽象,它对值列表执行一些操作以生成全新的值列表。它专注于过程循环遍历列表的每个项目以生成新列表这一事实,并忽略对列表的每个项目执行的实际操作。

所以我的问题是:抽象与概括有何不同?我正在寻找主要与函数式编程相关的答案。但是,如果在面向对象编程中有相似之处,那么我也想了解这些。

【问题讨论】:

  • 从数学上讲,抽象是存在量词,概括是全称量词。

标签: oop functional-programming abstraction nomenclature generalization


【解决方案1】:

确实是一个非常有趣的问题。我在这个话题上找到了this article,简明扼要地说:

虽然抽象通过隐藏不相关的细节来降低复杂性,但泛化通过用单个构造替换执行相似功能的多个实体来降低复杂性。

让我们以一个旧的图书馆图书管理系统为例。一本书有很多属性(页数、重量、字体大小、封面……),但对于我们图书馆的目的,我们可能只需要

Book(title, ISBN, borrowed)

我们只是从我们图书馆中的真实书籍中抽象出来,并且只在我们的应用程序的上下文中获取我们感兴趣的属性。


另一方面,泛化并不试图删除细节,而是使功能适用于更广泛(更通用)的项目范围。泛型容器是这种心态的一个很好的例子:你不会想写一个StringListIntList 等的实现,这就是为什么你宁愿写一个泛型适用于所有类型的列表(如 Scala 中的 List[T])。请注意,您没有抽象该列表,因为您没有删除任何细节或操作,您只是使它们普遍适用于您的所有类型。

第二轮

@dtldarek 的回答真是一个很好的例证!在此基础上,这里有一些代码可能会提供进一步的说明。

还记得我提到的Book 吗?当然,图书馆中还有其他可以借用的东西(我将所有这些对象的集合称为Borrowable,即使这可能甚至不是一个词:D):

所有这些项目在我们的数据库和业务逻辑中都有一个抽象表示,可能类似于我们的Book。此外,我们可能会定义一个所有Borrowables 共有的特征:

trait Borrowable {
    def itemId:Long
}

然后我们可以编写适用于所有Borrowables 的通用逻辑(此时我们不在乎它是一本书还是一本杂志):

object Library {
    def lend(b:Borrowable, c:Customer):Receipt = ...
    [...]
}

总结:我们在我们的数据库中存储了所有书籍、杂志和 DVD 的抽象表示,因为准确的表示既不可行也没有必要。然后我们继续说

客户是否借用了一本书、一本杂志或一张 DVD 并不重要。它始终是相同的过程。

因此我们概括借用物品的操作,将所有可以借用的东西定义为Borrowables。

【讨论】:

  • 感谢您的意见。它消除了我对泛化的一些疑虑。尽管如此,我对抽象与泛化的理解仍然有点模糊。也许您可以提供一些代码来解释差异?
  • @AaditMShah 再试一次,希望有帮助:)
  • 谢谢。它确实有帮助。我开始了额外的赏金来奖励你的答案,我会在一周内奖励给你,让你的答案得到最多的宣传。
  • 嘿,fresskoma,只是重温旧概念。因此,要明确抽象等同于临时多态性,而泛化等同于参数多态性。对吗?
【解决方案2】:

我会用一些例子来描述概括和抽象,我会参考this的文章。

据我所知,编程领域中抽象和泛化的定义没有官方来源(在我看来,维基百科可能是最接近官方定义的),所以我使用了一篇文章我认为是可信的。

泛化

文章指出:

"OOP中泛化的概念是指一个对象封装 一类对象的共同状态和行为。”

例如,如果你对形状应用泛化,那么所有类型形状的共同属性是面积和周长。

因此,广义形状(例如 Shape)和它的特化(例如圆形)可以用以下类表示(请注意,此图像取自上述文章)

类似地,如果您在喷气式飞机领域工作,您可以将 Jet 概括为具有翼展属性的喷气式飞机。喷气式飞机的专业化可能是战斗机,它将继承翼展属性并拥有自己的战斗机独有的属性,例如NumberOfMissiles。

抽象

文章将抽象定义为:

“识别具有系统性的常见模式的过程 变化;抽象代表通用模式并提供 一种指定使用哪种变体的方法“(理查德加布里埃尔)”

在编程领域:

抽象类是允许继承但可以 永远不会被实例化。

因此,在上面泛化部分给出的示例中,Shape 是抽象的:

在现实世界中,您永远不会计算面积或周长 通用形状,你必须知道你有什么样的几何形状 因为每种形状(例如正方形、圆形、矩形等)都有自己的 面积和周长公式。

然而,除了抽象之外,形状也是一种概括(因为它“封装了一类对象的常见状态和行为”,在这种情况下,对象就是形状)。 p>

回到我给出的关于 Jets 和 FighterJets 的示例,Jet 不是抽象的,因为 Jet 的具体实例是可行的,因为它可以存在于现实世界中,与形状不同,即在现实世界中你无法持有一个形状,你持有一个形状的实例,例如一个立方体。所以在飞机的例子中,喷气机不是抽象的,它是一种概括,因为它可能有一个喷气机的“具体”实例。

【讨论】:

    【解决方案3】:

    未解决可信/官方来源: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
    

    【讨论】:

      【解决方案4】:

      对象:

      抽象:

      概括:

      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++ 的重载函数形式)之间存在非平凡的联系某种意义上的抽象)。

      致谢: 传送门蛋糕Solodjttwo的甜品桌。 符号是来自material.io 的蛋糕图标。

      【讨论】:

      • @AaditMShah 让我知道 Haskell 代码是否有用,或者我是否应该删除它(这种方式的答案可读性较差)。
      • 不,Haskell 代码非常有用。不理解它的人可以直接跳过它而不会丢失上下文。
      • 为什么没有班级照片? :^) +1 顺便说一句
      • @dtldarek 因此,泛化涉及抽象,因为您在抽象出所有其他属性的同时,去掉了不同事物的一组共同属性。我的问题是,我们可以说聚合(“有”关系)也涉及抽象吗?具有成员腿、手臂、头、身体的类 Human 是抽象的吗?
      • @PatrikNusszer 我不确定我是否理解你的问题。假设你想创建一个函数来做一些有用的事情并适用于不同类型的对象。为此,您需要所有这些类型来为您提供通用 API,例如,每种类型 T 都提供返回数字的 .size。然后,即使没有指定抽象 .size 的抽象类或类型,该通用 API 也是您的泛化使用的(可能是隐含的和未指定的,但仍然是真实的)抽象。这能回答你的问题吗?
      【解决方案5】:

      让我用最简单的方式解释一下。

      “所有漂亮的女孩都是女性。”是一个抽象。

      “所有漂亮的女孩都会化妆。”是一种概括。

      【讨论】:

        【解决方案6】:

        抽象通常是通过消除不必要的细节来降低复杂性。例如,OOP 中的抽象类是一个父类,它包含其子级的共同特征,但没有指定确切的功能。

        泛化不一定需要避免细节,而是要有一些机制来允许将相同的函数应用于不同的参数。例如,函数式编程语言中的多态类型让您不必担心参数,而是专注于函数的操作。同样,在 java 中,您可以拥有泛型类型,它是所有类型的“保护伞”,而功能相同。

        【讨论】:

          【解决方案7】:

          抽象

          抽象是指定框架并隐藏实现级别信息。具体性将建立在抽象之上。它为您提供了在实施细节时可以遵循的蓝图。抽象通过隐藏底层细节来降低复杂性。

          示例:汽车的线框模型。

          泛化

          泛化使用从特化到泛化类的“is-a”关系。从专业到通用类都使用通用结构和行为。在更广泛的层面上,您可以将其理解为继承。为什么我使用继承这个词是,你可以很好地关联这个词。泛化也称为“Is-a”关系。

          示例:假设存在一个名为 Person 的类。学生是一个人。教师是一个人。所以这里的学生与人,师与人的关系是泛化的。

          【讨论】:

            【解决方案8】:

            我想为尽可能多的受众提供答案,因此我使用网络通用语言 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 不满意,因为它只适用于Arrays。我希望它适用于所有可折叠的数据类型:

            // 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 受益于抽象和泛化。

            【讨论】:

            • 只是为了确认,所以你认为像doesBrowserSupportTransparentImages()这样的函数是一个抽象?那么哪个功能不是呢?用“抽象”代替动词“提取方法”不是问题吗?
            • @Izhaki 是的,每个函数都是一个抽象,不管它的名字是什么。这个名字是你是否正确遵循关注点分离原则的一个指标。当你可以用一个或几个词来描述一个函数的语义时,你就很好了。否则你应该重构。
            猜你喜欢
            • 2015-09-26
            • 2010-10-10
            • 2010-09-26
            • 2014-09-21
            • 2011-01-18
            • 1970-01-01
            • 2010-12-27
            • 1970-01-01
            • 2011-01-24
            相关资源
            最近更新 更多