"Tóm tắt" nghĩa là gì?


95

Thường thì trong tài liệu Scala, tôi gặp cụm từ "trừu tượng qua", nhưng tôi không hiểu ý định của nó. Ví dụ , Martin Odersky viết

Bạn có thể chuyển các phương thức (hoặc "hàm") dưới dạng tham số hoặc bạn có thể trừu tượng hóa chúng. Bạn có thể chỉ định các kiểu dưới dạng tham số hoặc bạn có thể tóm tắt chúng.

Như một ví dụ khác, trong bài báo "Ngừng sử dụng mẫu trình quan sát" ,

Hệ quả từ các luồng sự kiện của chúng tôi là các giá trị hạng nhất là chúng tôi có thể trừu tượng hóa chúng.

Tôi đã đọc rằng generics đơn hàng đầu tiên "trừu tượng trên các loại", trong khi các đơn nguyên "trừu tượng trên các hàm tạo kiểu". Và chúng ta cũng thấy những cụm từ như thế này trong tờ giấy Cake Pattern . Để trích dẫn một trong nhiều ví dụ như vậy:

Các thành viên kiểu trừu tượng cung cấp một cách linh hoạt để trừu tượng hóa các kiểu cấu kiện cụ thể.

Ngay cả những câu hỏi liên quan đến tràn ngăn xếp cũng sử dụng thuật ngữ này. "không thể trừu tượng tồn tại trên kiểu được tham số hóa ..."

Vậy ... "trừu tượng qua" thực sự có nghĩa là gì?

Câu trả lời:


124

Trong đại số, cũng như trong quá trình hình thành khái niệm hàng ngày, các khái niệm trừu tượng được hình thành bằng cách nhóm các sự vật theo một số đặc điểm thiết yếu và bỏ qua các đặc điểm cụ thể khác của chúng. Sự trừu tượng được thống nhất dưới một biểu tượng hoặc từ biểu thị những điểm tương đồng. Chúng tôi nói rằng chúng tôi trừu tượng hóa những điểm khác biệt, nhưng điều này thực sự có nghĩa là chúng tôi đang tích hợp những điểm tương đồng.

Ví dụ, hãy xem xét một chương trình mà có tổng các số 1, 23:

val sumOfOneTwoThree = 1 + 2 + 3

Chương trình này không thú vị lắm, vì nó không trừu tượng lắm. Chúng tôi có thể tóm tắt các số mà chúng tôi đang tính tổng, bằng cách tích hợp tất cả danh sách các số dưới một biểu tượng duy nhất ns:

def sumOf(ns: List[Int]) = ns.foldLeft(0)(_ + _)

Và chúng tôi không đặc biệt quan tâm rằng đó cũng là một Danh sách. Danh sách là một phương thức khởi tạo kiểu cụ thể (nhận một kiểu và trả về một kiểu), nhưng chúng ta có thể trừu tượng hóa phương thức khởi tạo kiểu bằng cách chỉ định đặc tính thiết yếu nào chúng ta muốn (có thể gấp lại):

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)

Và chúng tôi có thể có các Foldablephiên bản ngầm cho Listvà bất kỳ thứ gì khác mà chúng tôi có thể gấp lại.

implicit val listFoldable = new Foldable[List] {
  def foldl[A, B](as: List[A], z: B, f: (B, A) => B) = as.foldLeft(z)(f)
}

val sumOfOneTwoThree = sumOf(List(1,2,3))

Hơn nữa, chúng ta có thể tóm tắt cả hoạt động và kiểu của toán hạng:

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)

Bây giờ chúng ta có một cái gì đó khá chung chung. Phương pháp mapReducenày sẽ gấp bất kỳ thứ nào F[A]cho trước mà chúng ta có thể chứng minh rằng Fcó thể gấp lại được và đó Alà một đơn nguyên hoặc có thể được ánh xạ thành một. Ví dụ:

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(List(4,5,6), Product)

Chúng tôi đã tóm tắt về monoids và gập lại được.


@coubeatczech Mã chạy trên REPL tốt. Bạn đang sử dụng phiên bản Scala nào và bạn đã gặp lỗi gì?
Daniel C. Sobral

1
@Apocalisp Sẽ rất thú vị nếu bạn tạo một trong hai ví dụ cuối cùng là một Sethoặc một số loại có thể gập lại khác. Một ví dụ với một Stringvà nối cũng sẽ khá tuyệt.
Daniel C. Sobral

1
Câu trả lời tuyệt vời, Runar. Cảm ơn! Tôi đã làm theo gợi ý của Daniel và tạo setFoldable và concatMonoid ngầm định mà không làm thay đổi mapReduce nào cả. Tôi đang trên đường tìm kiếm điều này.
Morgan Creighton

6
Tôi đã mất một chút thời gian để hiểu rằng trong 2 dòng cuối cùng bạn tận dụng thực tế là các đối tượng đồng hành Tổng và Sản phẩm, do chúng định nghĩa áp dụng (Int), được coi là Int => Tổng và Int => Sản phẩm của Scala trình biên dịch. Rất đẹp!
Kris Nuttycombe

Bài đăng hay :)! Trong ví dụ cuối cùng của bạn, logic ngầm Monoid có vẻ không cần thiết. Điều này đơn giản hơn: gist.github.com/cvogt/9716490
cvogt

11

Đối với một phép gần đúng đầu tiên, có thể "trừu tượng hóa" một cái gì đó có nghĩa là thay vì sử dụng trực tiếp cái gì đó, bạn có thể tạo một tham số của nó, hoặc sử dụng nó "ẩn danh".

Scala cho phép bạn trừu tượng hóa các kiểu, bằng cách cho phép các lớp, phương thức và giá trị có tham số kiểu và giá trị có kiểu trừu tượng (hoặc ẩn danh).

Scala cho phép bạn trừu tượng hóa các hành động, bằng cách cho phép các phương thức có tham số hàm.

Scala cho phép bạn tóm tắt các tính năng, bằng cách cho phép các kiểu được xác định theo cấu trúc.

Scala cho phép bạn tóm tắt các tham số kiểu, bằng cách cho phép các tham số kiểu bậc cao hơn.

Scala cho phép bạn tóm tắt các mẫu truy cập dữ liệu, bằng cách cho phép bạn tạo các trình trích xuất.

Scala cho phép bạn tóm tắt "những thứ có thể được sử dụng như một thứ khác", bằng cách cho phép chuyển đổi ngầm định dưới dạng tham số. Haskell làm tương tự với các lớp kiểu.

Scala không (chưa) cho phép bạn trừu tượng hóa trên các lớp. Bạn không thể chuyển một lớp cho một thứ gì đó, rồi sử dụng lớp đó để tạo các đối tượng mới. Các ngôn ngữ khác cho phép trừu tượng hóa trên các lớp.

("Đơn nguyên trừu tượng trên các hàm tạo kiểu" chỉ đúng theo một cách rất hạn chế. Đừng lo lắng về nó cho đến khi bạn có khoảnh khắc "Aha! Tôi hiểu đơn nguyên !!"

Khả năng trừu tượng hóa một số khía cạnh của tính toán về cơ bản là thứ cho phép sử dụng lại mã và cho phép tạo các thư viện chức năng. Scala cho phép trừu tượng hóa nhiều thứ hơn nhiều ngôn ngữ chính thống hơn và các thư viện trong Scala có thể mạnh hơn tương ứng.


1
Bạn có thể truyền a Manifest, hoặc thậm chí a Class, và sử dụng phản xạ để khởi tạo các đối tượng mới của lớp đó.
Daniel C. Sobral

6

Trừu tượng hóa là một loại tổng quát hóa.

http://en.wikipedia.org/wiki/Abstraction

Không chỉ trong Scala mà nhiều ngôn ngữ cần có những cơ chế như vậy để giảm độ phức tạp (hoặc ít nhất là tạo ra một hệ thống phân cấp phân chia thông tin thành các phần dễ hiểu hơn).

Một lớp là một trừu tượng trên một kiểu dữ liệu đơn giản. Nó giống như một loại cơ bản nhưng thực sự khái quát chúng. Vì vậy, một lớp không chỉ là một kiểu dữ liệu đơn giản mà còn có nhiều điểm chung với nó.

Khi anh ấy nói "trừu tượng hóa", anh ấy có nghĩa là quá trình mà bạn khái quát hóa. Vì vậy, nếu bạn đang trừu tượng hóa các phương thức dưới dạng tham số, bạn đang tổng quát hóa quá trình thực hiện điều đó. Ví dụ: thay vì truyền các phương thức cho các hàm, bạn có thể tạo ra một số kiểu tổng quát hóa để xử lý nó (chẳng hạn như không truyền các phương thức mà xây dựng một hệ thống đặc biệt để xử lý nó).

Trong trường hợp này, ông ấy có nghĩa là quá trình tóm tắt một vấn đề và tạo ra một giải pháp tương tự cho vấn đề. C có rất ít khả năng trừu tượng (bạn có thể làm điều đó nhưng nó rất lộn xộn và nhanh chóng và ngôn ngữ không hỗ trợ trực tiếp). Nếu bạn viết nó bằng C ++, bạn có thể sử dụng các khái niệm oop để giảm mức độ phức tạp của vấn đề (tốt, nó có cùng độ phức tạp nhưng việc hình thành khái niệm thường dễ dàng hơn (ít nhất một khi bạn học cách suy nghĩ về mặt trừu tượng)).

Ví dụ: Nếu tôi cần một kiểu dữ liệu đặc biệt giống như int nhưng, giả sử bị hạn chế, tôi có thể trừu tượng hóa nó bằng cách tạo một kiểu mới có thể được sử dụng như int nhưng có những thuộc tính tôi cần. Quá trình tôi sẽ sử dụng để làm một điều như vậy sẽ được gọi là "trừu tượng hóa".


5

Đây là chương trình hẹp của tôi và cho biết diễn giải. Nó tự giải thích và chạy trong 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()

1
khá nhiều ý tưởng "trừu tượng qua" bằng mã mạnh mẽ nhưng ngắn gọn, sẽ thử ngôn ngữ này +1
user44298

2

Các câu trả lời khác đã cung cấp một ý tưởng tốt về những loại trừu tượng tồn tại. Chúng ta hãy xem qua từng trích dẫn một và cung cấp một ví dụ:

Bạn có thể chuyển các phương thức (hoặc "hàm") dưới dạng tham số hoặc bạn có thể trừu tượng hóa chúng. Bạn có thể chỉ định các kiểu dưới dạng tham số hoặc bạn có thể tóm tắt chúng.

Truyền hàm dưới dạng tham số: List(1,-2,3).map(math.abs(x))Rõ ràng absđược truyền dưới dạng tham số ở đây. mapchính nó sẽ trừu tượng hóa một hàm thực hiện một điều đặc biệt nhất định với mỗi phần tử danh sách. val list = List[String]()chỉ định một tham số kiểu (Chuỗi). Bạn có thể viết một loại bộ sưu tập trong đó sử dụng các thành viên loại trừu tượng thay vì: val buffer = Buffer{ type Elem=String }. Một sự khác biệt là bạn phải viết def f(lis:List[String])...but def f(buffer:Buffer)..., vì vậy kiểu phần tử là loại "ẩn" trong phương thức thứ hai.

Hệ quả từ các luồng sự kiện của chúng tôi là các giá trị hạng nhất là chúng tôi có thể trừu tượng hóa chúng.

Trong Swing, một sự kiện chỉ "xảy ra" bất ngờ, và bạn phải đối phó với nó ở đây và bây giờ. Các luồng sự kiện cho phép bạn thực hiện tất cả hệ thống ống nước theo cách khai báo hơn. Ví dụ: khi bạn muốn thay đổi trình lắng nghe có trách nhiệm trong Swing, bạn phải hủy đăng ký cái cũ và đăng ký cái mới, đồng thời phải biết tất cả các chi tiết đẫm máu (ví dụ: các vấn đề về luồng). Với các luồng sự kiện, nguồn của các sự kiện trở thành một thứ mà bạn có thể chuyển qua một cách đơn giản, làm cho nó không khác lắm so với luồng byte hoặc luồng char, do đó có một khái niệm "trừu tượng" hơn.

Các thành viên kiểu trừu tượng cung cấp một cách linh hoạt để trừu tượng hóa các kiểu cấu kiện cụ thể.

Lớp Buffer ở trên đã là một ví dụ cho điều này.


0

Các câu trả lời ở trên cung cấp một lời giải thích tuyệt vời, nhưng để tóm tắt nó trong một câu duy nhất, tôi sẽ nói:

Tóm tắt nội dung nào đó cũng giống như bỏ qua nó ở những nơi không liên quan .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.