Tôi đã đọc A Tour of Scala: Các loại trừu tượng . Khi nào thì tốt hơn để sử dụng các loại trừu tượng?
Ví dụ,
abstract class Buffer {
type T
val element: T
}
đúng hơn là thuốc generic, ví dụ,
abstract class Buffer[T] {
val element: T
}
Tôi đã đọc A Tour of Scala: Các loại trừu tượng . Khi nào thì tốt hơn để sử dụng các loại trừu tượng?
Ví dụ,
abstract class Buffer {
type T
val element: T
}
đúng hơn là thuốc generic, ví dụ,
abstract class Buffer[T] {
val element: T
}
Câu trả lời:
Bạn có một quan điểm tốt về vấn đề này ở đây:
Mục đích của hệ thống loại của Scala
Cuộc trò chuyện với Martin Oderky, Phần III
của Bill Venners và Frank Sommers (ngày 18 tháng 5 năm 2009)
Cập nhật (tháng 10 năm 2009): những gì sau đây thực sự đã được minh họa trong bài viết mới này của Bill Venners:
Thành viên loại trừu tượng so với thông số loại chung trong Scala (xem tóm tắt ở cuối)
(Đây là trích xuất có liên quan của cuộc phỏng vấn đầu tiên, tháng 5 năm 2009, nhấn mạnh của tôi)
Luôn có hai khái niệm trừu tượng:
Trong Java bạn cũng có cả hai, nhưng nó phụ thuộc vào những gì bạn đang trừu tượng hóa.
Trong Java, bạn có các phương thức trừu tượng, nhưng bạn không thể truyền một phương thức làm tham số.
Bạn không có các trường trừu tượng, nhưng bạn có thể truyền một giá trị dưới dạng tham số.
Và tương tự, bạn không có các thành viên loại trừu tượng, nhưng bạn có thể chỉ định một loại làm tham số.
Vì vậy, trong Java bạn cũng có tất cả ba trong số này, nhưng có một sự khác biệt về nguyên tắc trừu tượng mà bạn có thể sử dụng cho những loại điều gì. Và bạn có thể lập luận rằng sự khác biệt này là khá độc đoán.
Chúng tôi quyết định có cùng các nguyên tắc xây dựng cho cả ba loại thành viên .
Vì vậy, bạn có thể có các trường trừu tượng cũng như các tham số giá trị.
Bạn có thể truyền các phương thức (hoặc "hàm") làm tham số hoặc bạn có thể trừu tượng hóa chúng.
Bạn có thể chỉ định các loại làm tham số hoặc bạn có thể trừu tượng hóa chúng.
Và những gì chúng ta có được về mặt khái niệm là chúng ta có thể mô hình hóa cái này theo cái khác. Ít nhất về nguyên tắc, chúng ta có thể diễn tả mọi loại tham số hóa như một dạng trừu tượng hướng đối tượng. Vì vậy, theo một nghĩa nào đó, bạn có thể nói Scala là một ngôn ngữ trực giao và hoàn chỉnh hơn.
Đặc biệt, những loại trừu tượng mua cho bạn là một cách xử lý tốt cho những vấn đề hiệp phương sai mà chúng ta đã nói trước đây.
Một vấn đề tiêu chuẩn, đã có từ lâu, là vấn đề về động vật và thực phẩm.
Câu đố là có một lớp học Animal
với một phương pháp eat
, trong đó ăn một số thực phẩm.
Vấn đề là nếu chúng ta phân lớp Thú và có một lớp như Bò, thì chúng sẽ chỉ ăn Cỏ và không ăn thức ăn tùy tiện. Chẳng hạn, một con bò không thể ăn một con cá.
Điều bạn muốn là có thể nói rằng Bò có phương pháp ăn chỉ ăn Cỏ chứ không phải những thứ khác.
Trên thực tế, bạn không thể làm điều đó trong Java vì hóa ra bạn có thể xây dựng các tình huống không có căn cứ, như vấn đề gán Fruit cho một biến Apple mà tôi đã nói trước đó.
Câu trả lời là bạn thêm một loại trừu tượng vào lớp Animal .
Bạn nói, lớp Thú mới của tôi có một loại SuitableFood
mà tôi không biết.
Vì vậy, nó là một loại trừu tượng. Bạn không cung cấp cho việc thực hiện các loại. Sau đó, bạn có một eat
phương pháp chỉ ăn SuitableFood
.
Và sau đó trong Cow
lớp tôi sẽ nói, OK, tôi có một con Bò, mở rộng lớp học Animal
, và cho Cow type SuitableFood equals Grass
.
Vì vậy, các kiểu trừu tượng cung cấp khái niệm này về một loại trong siêu lớp mà tôi không biết, sau đó tôi sẽ điền vào các lớp con với những điều tôi biết .
Quả thực bạn có thể. Bạn có thể tham số hóa lớp Thú với loại thức ăn mà nó ăn.
Nhưng trong thực tế, khi bạn làm điều đó với nhiều thứ khác nhau, nó sẽ dẫn đến sự bùng nổ của các tham số , và thông thường, những gì nhiều hơn, trong giới hạn của các tham số .
Tại ECOOP năm 1998, Kim Bruce, Phil Wadler và tôi đã có một bài báo mà chúng tôi đã chỉ ra rằng khi bạn tăng số lượng những điều bạn không biết, chương trình điển hình sẽ phát triển theo phương pháp bậc hai .
Vì vậy, có những lý do rất chính đáng để không thực hiện các tham số, nhưng để có các thành viên trừu tượng này, bởi vì họ không cho bạn phương thức bậc hai này.
thatismatt hỏi trong các ý kiến:
Bạn có nghĩ rằng sau đây là một bản tóm tắt công bằng:
- Các loại trừu tượng được sử dụng trong các mối quan hệ 'has-a' hoặc 'used-a' (ví dụ a
Cow eats Grass
)- trong đó như chung chung thường là 'của' mối quan hệ (ví dụ
List of Ints
)
Tôi không chắc chắn mối quan hệ đó là khác nhau giữa việc sử dụng các loại trừu tượng hoặc khái quát. Điều khác biệt là:
Để hiểu Martin đang nói về điều gì khi nói đến "sự bùng nổ của các tham số, và thông thường, những gì nhiều hơn, trong giới hạn của các tham số " và sự tăng trưởng theo phương pháp bậc hai của nó khi kiểu trừu tượng được mô hình hóa bằng cách sử dụng tổng quát, bạn có thể xem xét bài viết " Trừu tượng thành phần có thể mở rộng "Được viết bởi ... Martin Oderky và Matthias Zenger cho OOPSLA 2005, được tham chiếu trong các ấn phẩm của dự án Palcom (hoàn thành năm 2007).
Chất chiết xuất có liên quan
Các thành viên loại trừu tượng cung cấp một cách linh hoạt để trừu tượng hơn các loại thành phần cụ thể.
Các loại trừu tượng có thể ẩn thông tin về phần bên trong của một thành phần, tương tự như việc sử dụng chúng trong chữ ký SML . Trong một khung hướng đối tượng nơi các lớp có thể được mở rộng bằng sự kế thừa, chúng cũng có thể được sử dụng như một phương tiện tham số linh hoạt (thường được gọi là đa hình gia đình, ví dụ , xem mục weblog này và bài báo được viết bởi Eric Ernst ).
(Lưu ý: Đa hình gia đình đã được đề xuất cho các ngôn ngữ hướng đối tượng như là một giải pháp để hỗ trợ các lớp đệ quy lẫn nhau có thể tái sử dụng nhưng an toàn.
Một ý tưởng chính của đa hình gia đình là khái niệm các gia đình, được sử dụng để nhóm các lớp đệ quy lẫn nhau
abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}
Ở đây, khai báo kiểu T bị ràng buộc bởi một kiểu trên bị ràng buộc bao gồm một tên lớp được đặt hàng và một sàng lọc
{ type O = T }
.
Chủ trương hạn chế trên ràng buộc các chuyên ngành của T trong lớp con cho những phân nhóm của Ordered mà loại thành viênO
củaequals T
.
Do ràng buộc này,<
phương thức của lớp được đặt hàng được đảm bảo có thể áp dụng cho người nhận và đối số của loại T.
Ví dụ cho thấy rằng thành viên loại bị ràng buộc có thể tự xuất hiện như một phần của ràng buộc.
(tức là Scala hỗ trợ đa hình giới hạn F )
(Lưu ý, từ Peter Canning, William Cook, Walter Hill, Walter Olthoff:
Định lượng giới hạn được giới thiệu bởi Cardelli và Wegner như một phương tiện gõ các hàm hoạt động thống nhất trên tất cả các kiểu con của một loại nhất định.
Họ định nghĩa một mô hình "đối tượng" đơn giản. và sử dụng lượng bị chặn để gõ kiểm tra chức năng đó có ý nghĩa trên tất cả các đối tượng có một bộ quy định của "thuộc tính".
một bài thuyết trình thực tế hơn về ngôn ngữ hướng đối tượng sẽ cho phép các đối tượng là những yếu tố của các loại đệ quy được xác định .
trong bối cảnh này, giáp định lượng hóa không còn phục vụ mục đích dự định của nó. Dễ dàng tìm thấy các hàm có ý nghĩa trên tất cả các đối tượng có một bộ phương thức xác định, nhưng không thể gõ vào hệ thống Cardelli-Wegner.
Để cung cấp cơ sở cho các hàm đa hình được gõ trong các ngôn ngữ hướng đối tượng, chúng tôi giới thiệu định lượng giới hạn F)
Có hai hình thức trừu tượng chính trong các ngôn ngữ lập trình:
Hình thức đầu tiên là điển hình cho các ngôn ngữ chức năng, trong khi hình thức thứ hai thường được sử dụng trong các ngôn ngữ hướng đối tượng.
Theo truyền thống, Java hỗ trợ tham số hóa cho các giá trị và trừu tượng hóa thành viên cho các hoạt động. Java 5.0 gần đây hơn với generic cũng hỗ trợ tham số hóa cho các loại.
Các đối số để bao gồm các tổng quát trong Scala là hai lần:
Đầu tiên, mã hóa thành các loại trừu tượng không đơn giản để làm bằng tay. Bên cạnh sự mất mát về tính đồng nhất, còn có vấn đề xung đột tên ngẫu nhiên giữa các tên loại trừu tượng mô phỏng các tham số loại.
Thứ hai, các loại khái quát và trừu tượng thường phục vụ các vai trò riêng biệt trong các chương trình Scala.
Trong một hệ thống có tính đa hình giới hạn, việc viết lại kiểu trừu tượng thành khái quát có thể kéo theo sự mở rộng bậc hai của giới hạn kiểu .
Thành viên loại trừu tượng so với thông số loại chung trong Scala (Bill Venners)
(nhấn mạnh của tôi)
Quan sát của tôi cho đến nay về các thành viên loại trừu tượng là họ chủ yếu là một lựa chọn tốt hơn so với các tham số loại chung khi:
- bạn muốn cho phép mọi người trộn lẫn trong các định nghĩa về các loại đó thông qua các đặc điểm .
- bạn nghĩ rằng việc đề cập rõ ràng về tên thành viên loại khi nó được xác định sẽ giúp đọc mã .
Thí dụ:
nếu bạn muốn vượt qua ba đối tượng vật cố khác nhau vào các thử nghiệm, bạn sẽ có thể làm như vậy, nhưng bạn sẽ cần chỉ định ba loại, một loại cho mỗi tham số. Do đó, tôi đã thực hiện cách tiếp cận tham số kiểu, các lớp bộ của bạn có thể đã trông giống như thế này:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
Trong khi đó với cách tiếp cận thành viên kiểu sẽ như thế này:
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
Một sự khác biệt nhỏ khác giữa các thành viên loại trừu tượng và các tham số loại chung là khi một tham số loại chung được chỉ định, các trình đọc mã không nhìn thấy tên của tham số loại. Vì vậy, đã có người nhìn thấy dòng mã này:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
Họ sẽ không biết tên của tham số loại được chỉ định là StringBuilder là gì mà không tìm kiếm nó. Trong khi đó tên của tham số loại nằm ngay trong mã theo cách tiếp cận thành viên loại trừu tượng:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
Trong trường hợp sau, người đọc mã có thể thấy đó
StringBuilder
là loại "tham số vật cố".
Họ vẫn sẽ cần phải tìm ra "tham số vật cố" nghĩa là gì, nhưng ít nhất họ có thể có được tên của loại mà không cần xem tài liệu.
Tôi đã có cùng một câu hỏi khi tôi đọc về Scala.
Lợi thế của việc sử dụng thuốc generic là bạn đang tạo ra một họ các loại. Không ai sẽ cần phải phân lớp Buffer
-they chỉ có thể sử dụng Buffer[Any]
, Buffer[String]
vv
Nếu bạn sử dụng một loại trừu tượng, thì mọi người sẽ buộc phải tạo một lớp con. Mọi người sẽ cần các lớp học như AnyBuffer
, StringBuffer
vv
Bạn cần phải quyết định cái nào tốt hơn cho nhu cầu cụ thể của bạn.
Buffer { type T <: String }
hoặc Buffer { type T = String }
tùy thuộc vào nhu cầu của bạn
Bạn có thể sử dụng các loại trừu tượng kết hợp với các tham số loại để thiết lập các mẫu tùy chỉnh.
Giả sử bạn cần thiết lập một mẫu có ba đặc điểm được kết nối:
trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
theo cách mà các đối số được đề cập trong các tham số loại là chính AA, BB, CC
Bạn có thể đi kèm với một số loại mã:
trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
mà sẽ không hoạt động theo cách đơn giản này vì các liên kết tham số loại. Bạn cần làm cho nó biến đổi để kế thừa chính xác
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
Mẫu này sẽ biên dịch nhưng nó đặt ra các yêu cầu mạnh mẽ về quy tắc phương sai và không thể được sử dụng trong một số trường hợp
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
def forth(x:B):C
def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
def forth(x:C):A
def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
def forth(x:A):B
def back(x:B):A
}
Trình biên dịch sẽ phản đối với một loạt các lỗi kiểm tra phương sai
Trong trường hợp đó, bạn có thể thu thập tất cả các yêu cầu loại trong tính trạng bổ sung và tham số hóa các đặc điểm khác đối với nó
//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
type A <: AA[O]
type B <: BB[O]
type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
type A = O#A
type B = O#B
type C = O#C
def left(l:B):C
def right(r:C):B = r.left(this)
def join(l:B, r:C):A
def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
type A = O#A
type B = O#B
type C = O#C
def left(l:C):A
def right(r:A):C = r.left(this)
def join(l:C, r:A):B
def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
type A = O#A
type B = O#B
type C = O#C
def left(l:A):B
def right(r:B):A = r.left(this)
def join(l:A, r:B):C
def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}
Bây giờ chúng ta có thể viết biểu diễn cụ thể cho mẫu được mô tả, xác định các phương thức bên trái và tham gia trong tất cả các lớp và được quyền và nhân đôi miễn phí
class ReprO extends OO[ReprO] {
override type A = ReprA
override type B = ReprB
override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
override def left(l:B):C = ReprC(data - l.data)
override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
override def left(l:C):A = ReprA(data - l.data)
override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
override def left(l:A):B = ReprB(data - l.data)
override def join(l:A, r:B):C = ReprC(l.data + r.data)
}
Vì vậy, cả kiểu trừu tượng và tham số kiểu được sử dụng để tạo trừu tượng. Cả hai đều có điểm yếu và điểm mạnh. Các loại trừu tượng cụ thể hơn và có khả năng mô tả bất kỳ cấu trúc kiểu nào nhưng dài dòng và yêu cầu phải được chỉ định rõ ràng. Các tham số loại có thể tạo ra một loạt các loại ngay lập tức nhưng cung cấp cho bạn thêm lo lắng về tính kế thừa và giới hạn loại.
Chúng mang lại sức mạnh tổng hợp cho nhau và có thể được sử dụng kết hợp để tạo ra sự trừu tượng phức tạp không thể chỉ bằng một trong số chúng.
Tôi nghĩ rằng không có nhiều sự khác biệt ở đây. Kiểu thành viên trừu tượng có thể được xem như là kiểu tồn tại tương tự như kiểu ghi trong một số ngôn ngữ chức năng khác.
Ví dụ: chúng tôi có:
class ListT {
type T
...
}
và
class List[T] {...}
Sau đó, ListT
cũng giống như List[_]
. Sự kiên định của các thành viên loại là chúng ta có thể sử dụng lớp mà không cần loại cụ thể rõ ràng và tránh quá nhiều tham số loại.