抽象化とは、何かをより具体化し、より抽象化することだと理解しています。その何かは、データ構造またはプロシージャのいずれかです。例えば:
map
は、値のリストに対していくつかの操作を実行して、まったく新しい値のリストを生成する手続きの抽象化です。これは、プロシージャがリストのすべてのアイテムをループして新しいリストを生成し、リストのすべてのアイテムに対して実行される実際の操作を無視するという事実に集中しています。だから私の質問はこれです:抽象化は一般化とどう違うのですか?主に関数型プログラミングに関連する答えを探しています。ただし、オブジェクト指向プログラミングに類似点がある場合は、それらについても学びたいと思います。
オブジェクト:
抽象化:
汎化:
Haskellの例:
3つの異なるインターフェースで優先度キューを使用することによる選択ソートの実装:
{-# 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)がnonxP(x)を暗示するという事実(非空のユニバース内)と、抽象化のない一般化がめったにないこと(c ++のようなオーバーロードされた関数の形式でさえも)ある意味での一種の抽象化)。
クレジット:Solo によるポータルケーキ。 djttwo によるデザートテーブル。シンボルは material.io のケーキアイコンです。
非常に興味深い質問です。私はこのトピックについて この記事 を見つけました、それは簡潔に述べています:
抽象化は無関係な詳細を隠すことで複雑さを軽減しますが、一般化は、類似の機能を実行する複数のエンティティを単一の構成で置き換えることで複雑さを軽減します。
図書館の本を管理するシステムの古い例を見てみましょう。本には多くのプロパティ(ページ数、重さ、フォントサイズ、表紙など)がありますが、ライブラリの目的には、
Book(title, ISBN, borrowed)
私たちは、ライブラリーの実際の本から抽象化し、アプリケーションのコンテキストに関心のあるプロパティのみを取得しました。
一方、汎化は詳細を削除しようとするのではなく、より幅広い(より一般的な)項目に機能を適用できるようにします。一般的なコンテナーは、その考え方の非常に良い例です。StringList
、IntList
などの実装を記述したくないので、genericすべてのタイプに適用されるリスト(ScalaのList[T]
など)。リストを抽象化していないことに注意してください。詳細や操作は削除していないため、すべてのタイプに一般的に適用できるようにしただけです。
@dtldarekの答えは本当にとても良いイラストです!これに基づいて、さらに明確にするためのコードを次に示します。
私が言及したBook
を覚えていますか?もちろん、ライブラリには他にも借りることができるものがあります(私はそれらすべてのオブジェクトのセットをBorrowable
と呼びますが、それはおそらくWord:Dでもありません)。
これらのすべてのアイテムには、データベースとビジネスロジックでabstract表現があり、おそらくBook
と同様です。さらに、すべてのBorrowable
sに共通の特性を定義する場合があります。
trait Borrowable {
def itemId:Long
}
次に、すべてのBorrowable
sに適用されるgeneralizedロジックを記述できます(その時点では、本か雑誌かは関係ありません):
object Library {
def lend(b:Borrowable, c:Customer):Receipt = ...
[...]
}
要約すると、正確な表現が実行可能でも必要でもないため、データベースにすべての本、雑誌、DVDの抽象表現を保存しました。それから私たちは先に行き、言った
本、雑誌、DVDのどちらをお借りしても構いません。それは常に同じプロセスです。
したがって、私たちはgeneralized借りることができるすべてのものをBorrowable
sとして定義することにより、アイテムを借りる操作を行います。
いくつかの例を使用して、一般化と抽象化について説明し、 this の記事を参照します。
私の知る限りでは、プログラミングドメインでの抽象化と一般化の定義の公式な情報源はありません(おそらく、Wikipediaは私の意見では公式の定義に最も近いと思われます)。代わりに、私が考える記事を使用しました。信頼できる。
汎化
記事はこう述べています:
「OOPにおける一般化の概念は、オブジェクトがオブジェクトのカテゴリの一般的な状態と動作をカプセル化することを意味します。」
したがって、たとえば、形状に一般化を適用する場合、すべてのタイプの形状に共通のプロパティは面積と周囲長です。
したがって、一般化された形状(例:形状)とその特殊化(例:円)は、次のようにクラスで表すことができます(この画像は前述の記事から引用したものです)
同様に、ジェット機の領域で作業している場合、一般化としてジェットを使用できます。これには翼幅プロパティがあります。ジェットの特殊化は、翼幅プロパティを継承し、戦闘機に固有の独自のプロパティを持つ戦闘機などです。 NumberOfMissiles。
抽象化
この記事では、抽象化を次のように定義しています。
「体系的なバリエーションを持つ共通パターンを識別するプロセス。抽象化は共通パターンを表し、どのバリエーションを使用するかを指定する手段を提供します」(Richard Gabriel) "
プログラミングの領域で:
抽象クラスは継承を許可するが、インスタンス化することはできない親クラスです。
したがって、上記の一般化セクションで示した例では、Shapeは次のように抽象的です。
現実の世界では、一般的な形状の面積や周長を計算することは決してありません。各形状(正方形、円、長方形など)には独自の面積と周長の式があるため、どのような幾何学形状を持っているかを知る必要があります。
ただし、形状は抽象的であるだけでなく、一般化でもあります(「オブジェクトのカテゴリの一般的な状態と動作をカプセル化する」ため、この場合、オブジェクトは形状です)。
JetsとFighterJetsについて説明した例に戻ると、Jetの具体的なインスタンスは実現可能であるため、Jetは抽象的ではありません。形状のインスタンスを保持する立方体。したがって、航空機の例では、Jetは抽象的ではなく、Jetの「具体的な」インスタンスを持つことが可能なため、それは一般化されています。
信頼できる/公式のソースを扱っていない: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
私は可能な限り多くの聴衆に答えを提供したいので、私はウェブのJava言語であるLingua Francaを使用します。
まず、通常の命令コードから始めましょう。
// 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
sでしか機能しないため、Array
にはまだ不満があります。折りたたみ可能なすべてのデータ型で動作するようにしたい:
// 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) // []
);
I アプリケーションを拡張 of concatMap
は、データ型のより大きなドメインを包含し、すべての折りたたみ可能なデータ型を命名します。一般化は、さまざまなタイプ(またはオブジェクト、エンティティ)間の共通点を強調します。
これは、辞書を渡すことで実現しました(この例では、concatMap
の追加の引数)。現在、これらの型のディクショナリをコード全体に渡すのはやや面倒です。したがって、Haskellの人々は、型クラスをum型の辞書に抽象化しました。
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
は、抽象化と一般化の両方からメリットを得ます。
抽象化
抽象化とは、フレームワークを指定し、実装レベルの情報を隠すことです。抽象性の上に具体性が構築されます。詳細を実装する際に従うべき青写真を提供します。抽象化は、低レベルの詳細を隠すことで複雑さを軽減します。
例:車のワイヤーフレームモデル。
汎化
汎化では、特殊化から汎化クラスまでの「is-a」関係を使用します。特殊化から一般化されたクラスまで、共通の構造と動作が使用されます。非常に広いレベルでは、これを継承として理解できます。私が継承という用語を採用する理由は、この用語を非常にうまく関連付けることができます。一般化は「Is-a」関係とも呼ばれます。
例:Personという名前のクラスが存在するとします。学生は人です。学部は人です。したがって、ここでは学生と人、同様に学部と人との関係が一般化されています。
できるだけ簡単に説明させてください。
「すべてのかわいい女の子は女性です。」抽象化です。
「すべての可愛い女の子が化粧をしています。」一般化です。
抽象化は通常、不要な詳細を排除することで複雑さを軽減することです。たとえば、OOPの抽象クラスは、その子の共通機能を含む親クラスですが、正確な機能を指定していません。
一般化では、必ずしも詳細を回避する必要はありませんが、同じ関数を異なる引数に適用できるようにするためのメカニズムが必要です。たとえば、関数型プログラミング言語のポリモーフィック型を使用すると、引数に煩わされることなく、関数の操作に集中できます。同様に、Javaでは、関数は同じであるが、すべての型の「傘」であるジェネリック型を持つことができます。