Tổng quat
Lập trình cấp kiểu có nhiều điểm tương đồng với lập trình cấp giá trị, truyền thống. Tuy nhiên, không giống như lập trình cấp giá trị, trong đó tính toán xảy ra trong thời gian chạy, trong lập trình cấp kiểu, tính toán xảy ra tại thời gian biên dịch. Tôi sẽ cố gắng vẽ ra sự tương đồng giữa lập trình ở cấp giá trị và lập trình ở cấp kiểu.
Mô hình
Có hai mô hình chính trong lập trình cấp kiểu: "hướng đối tượng" và "chức năng". Hầu hết các ví dụ được liên kết đến từ đây đều tuân theo mô hình hướng đối tượng.
Có thể tìm thấy một ví dụ hay, khá đơn giản về lập trình cấp kiểu trong mô hình hướng đối tượng trong cách triển khai apocalisp của phép tính lambda , được sao chép tại đây:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
Như có thể thấy trong ví dụ, mô hình hướng đối tượng cho lập trình cấp kiểu tiến hành như sau:
- Đầu tiên: xác định một đặc điểm trừu tượng với các trường kiểu trừu tượng khác nhau (xem bên dưới để biết trường trừu tượng là gì). Đây là một mẫu để đảm bảo rằng các trường loại nhất định tồn tại trong tất cả các triển khai mà không bắt buộc triển khai. Trong ví dụ phép tính lambda, tương ứng này để
trait Lambda
đảm bảo rằng các loại sau đây tồn tại: subst
, apply
, vàeval
.
- Tiếp theo: xác định các chuyển con mở rộng đặc điểm trừu tượng và triển khai các trường kiểu trừu tượng khác nhau
- Thông thường, các chuyển con này sẽ được tham số hóa bằng các đối số. Trong ví dụ giải tích lambda, các kiểu con
trait App extends Lambda
được tham số hóa với hai kiểu ( S
và T
, cả hai đều phải là kiểu con của Lambda
), được trait Lam extends Lambda
tham số hóa với một kiểu ( T
) vàtrait X extends Lambda
(không được tham số hóa).
- các trường kiểu thường được triển khai bằng cách tham chiếu đến các tham số kiểu của trang con và đôi khi tham chiếu đến các trường kiểu của chúng thông qua toán tử băm:
#
(rất giống với toán tử dấu chấm: .
cho các giá trị). Trong đặc điểm App
của ví dụ phép tính lambda, loại eval
được thực hiện như sau: type eval = S#eval#apply[T]
. Đây thực chất là gọi eval
kiểu tham số của đặc điểm S
và gọi apply
với tham số T
trên kết quả. Lưu ý, S
được đảm bảo có một eval
kiểu vì tham số chỉ định nó là một kiểu con của Lambda
. Tương tự, kết quả của eval
phải có một apply
kiểu, vì nó được chỉ định là một kiểu con của Lambda
, như được chỉ định trong đặc điểm trừu tượng Lambda
.
Mô hình chức năng bao gồm việc xác định nhiều hàm tạo kiểu tham số hóa không được nhóm lại với nhau trong các đặc điểm.
So sánh giữa lập trình cấp giá trị và lập trình cấp kiểu
- lớp trừu tượng
- cấp giá trị:
abstract class C { val x }
- loại-cấp:
trait C { type X }
- các loại phụ thuộc đường dẫn
C.x
(tham chiếu đến giá trị trường / hàm x trong đối tượng C)
C#x
(tham chiếu loại trường x trong đặc điểm C)
- chữ ký hàm (không có triển khai)
- cấp giá trị:
def f(x:X) : Y
- type-level:
type f[x <: X] <: Y
(đây được gọi là "phương thức tạo kiểu" và thường xuất hiện trong đặc điểm trừu tượng)
- thực hiện chức năng
- cấp giá trị:
def f(x:X) : Y = x
- loại-cấp:
type f[x <: X] = x
- điều kiện
- kiểm tra sự bình đẳng
- cấp giá trị:
a:A == b:B
- loại-cấp:
implicitly[A =:= B]
- cấp giá trị: Xảy ra trong JVM thông qua kiểm tra đơn vị trong thời gian chạy (nghĩa là không có lỗi thời gian chạy):
- trong essense là một khẳng định:
assert(a == b)
- type-level: Xảy ra trong trình biên dịch qua lỗi đánh máy (tức là không có lỗi trình biên dịch):
- về bản chất là một so sánh kiểu: ví dụ
implicitly[A =:= B]
A <:< B
, chỉ biên dịch nếu A
là một kiểu con củaB
A =:= B
, chỉ biên dịch nếu A
là một kiểu con của B
và B
là một kiểu con củaA
A <%< B
, ("có thể xem dưới dạng") chỉ biên dịch khi A
có thể xem dưới dạng B
(tức là có một chuyển đổi ngầm định từ A
thành một loại phụ của B
)
- một ví dụ
- nhiều toán tử so sánh hơn
Chuyển đổi giữa các loại và giá trị
Trong nhiều ví dụ, các kiểu được xác định thông qua các đặc điểm thường vừa trừu tượng vừa được niêm phong, và do đó không thể được khởi tạo trực tiếp hoặc thông qua lớp con ẩn danh. Vì vậy, nó thường được sử dụng null
làm giá trị trình giữ chỗ khi thực hiện tính toán cấp giá trị bằng cách sử dụng một số loại sở thích:
- ví dụ
val x:A = null
, đâu A
là kiểu bạn quan tâm
Do tính năng xóa kiểu, các kiểu được tham số hóa đều trông giống nhau. Hơn nữa, (như đã đề cập ở trên) các giá trị bạn đang làm việc đều có xu hướng giống nhau null
, và do đó việc điều chỉnh kiểu đối tượng (ví dụ: thông qua một câu lệnh so khớp) là không hiệu quả.
Bí quyết là sử dụng các hàm và giá trị ngầm định. Trường hợp cơ sở thường là một giá trị ngầm định và trường hợp đệ quy thường là một hàm không tường minh. Thật vậy, lập trình mức kiểu sử dụng nhiều hàm ý.
Hãy xem xét ví dụ này ( lấy từ metascala và apocalisp ):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
Ở đây bạn có một bảng mã peano của các số tự nhiên. Có nghĩa là, bạn có một kiểu cho mỗi số nguyên không âm: một kiểu đặc biệt cho 0, cụ thể là _0
; và mỗi số nguyên lớn hơn 0 có một kiểu biểu mẫu Succ[A]
, trong đó A
kiểu biểu diễn một số nguyên nhỏ hơn. Ví dụ, kiểu đại diện cho 2 sẽ là: Succ[Succ[_0]]
(kế tiếp áp dụng hai lần cho kiểu đại diện cho số không).
Chúng ta có thể đặt bí danh cho các số tự nhiên khác nhau để tiện tham khảo hơn. Thí dụ:
type _3 = Succ[Succ[Succ[_0]]]
(Điều này rất giống với việc xác định a val
là kết quả của một hàm.)
Bây giờ, giả sử chúng ta muốn xác định một hàm cấp giá trị def toInt[T <: Nat](v : T)
nhận vào một giá trị đối số v
, phù hợp Nat
và trả về một số nguyên đại diện cho số tự nhiên được mã hóa trong v
kiểu của. Ví dụ: nếu chúng ta có giá trị val x:_3 = null
( null
kiểu Succ[Succ[Succ[_0]]]
), chúng ta muốn toInt(x)
trả về 3
.
Để triển khai toInt
, chúng ta sẽ sử dụng lớp sau:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
Như chúng ta sẽ thấy bên dưới, sẽ có một đối tượng được xây dựng từ lớp TypeToValue
cho mỗi Nat
từ _0
lên đến (ví dụ) _3
, và mỗi đối tượng sẽ lưu trữ biểu diễn giá trị của kiểu tương ứng (tức là TypeToValue[_0, Int]
sẽ lưu giá trị 0
, TypeToValue[Succ[_0], Int]
sẽ lưu giá trị 1
, v.v.). Lưu ý, TypeToValue
được tham số hóa bởi hai loại: T
và VT
. T
tương ứng với loại mà chúng tôi đang cố gắng chỉ định giá trị (trong ví dụ của chúng tôi, Nat
) và VT
tương ứng với loại giá trị mà chúng tôi đang gán cho nó (trong ví dụ của chúng tôi,Int
).
Bây giờ chúng ta đưa ra hai định nghĩa ngầm định sau:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
Và chúng tôi thực hiện toInt
như sau:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
Để hiểu cách toInt
hoạt động, chúng ta hãy xem xét những gì nó hoạt động trên một số đầu vào:
val z:_0 = null
val y:Succ[_0] = null
Khi chúng ta gọi toInt(z)
, trình biên dịch sẽ tìm kiếm một đối số ngầm định ttv
về kiểu TypeToValue[_0, Int]
(vì z
là kiểu _0
). Nó tìm thấy đối tượng _0ToInt
, nó gọi getValue
phương thức của đối tượng này và lấy lại0
. Điểm quan trọng cần lưu ý là chúng tôi đã không chỉ định cho chương trình sử dụng đối tượng nào, trình biên dịch đã ngầm hiểu.
Bây giờ chúng ta hãy xem xét toInt(y)
. Lần này, trình biên dịch tìm kiếm một đối số ngầm định ttv
về kiểu TypeToValue[Succ[_0], Int]
(vì y
là kiểu Succ[_0]
). Nó tìm hàm succToInt
, có thể trả về một đối tượng có kiểu thích hợp ( TypeToValue[Succ[_0], Int]
) và đánh giá nó. Bản thân hàm này nhận một đối số ngầm định ( v
) kiểu TypeToValue[_0, Int]
(nghĩa là, TypeToValue
tham số kiểu đầu tiên có một ít hơn Succ[_]
). Trình biên dịch cung cấp _0ToInt
(như đã được thực hiện trong phần đánh giá toInt(z)
ở trên) và succToInt
xây dựng một TypeToValue
đối tượng mới có giá trị 1
. Một lần nữa, điều quan trọng cần lưu ý là trình biên dịch đang cung cấp tất cả các giá trị này một cách ngầm định, vì chúng ta không có quyền truy cập vào chúng một cách rõ ràng.
Kiểm tra công việc của bạn
Có một số cách để xác minh rằng các tính toán cấp loại của bạn đang làm những gì bạn mong đợi. Dưới đây là một số cách tiếp cận. Tạo hai loại A
và B
, mà bạn muốn xác minh là bằng nhau. Sau đó, kiểm tra xem biên dịch sau:
Equal[A, B]
implicitly[A =:= B]
Ngoài ra, bạn có thể chuyển đổi kiểu thành một giá trị (như được hiển thị ở trên) và thực hiện kiểm tra thời gian chạy của các giá trị. Ví dụ: assert(toInt(a) == toInt(b))
đâu a
là loại A
và b
thuộc loại B
.
Tài nguyên bổ sung
Bạn có thể tìm thấy toàn bộ các cấu trúc có sẵn trong phần loại của sổ tay tham khảo scala (pdf) .
Adriaan Moors có một số bài báo học thuật về các hàm tạo kiểu và các chủ đề liên quan với các ví dụ từ scala:
Apocalisp là một blog có nhiều ví dụ về lập trình cấp kiểu trong scala.
- Lập trình cấp kiểu trong Scala là một chuyến tham quan có hướng dẫn tuyệt vời về một số lập trình cấp kiểu bao gồm boolean, số tự nhiên (như trên), số nhị phân, danh sách không đồng nhất và hơn thế nữa.
- Thêm Scala Typehackery là triển khai tính toán lambda ở trên.
ScalaZ là một dự án rất tích cực đang cung cấp chức năng mở rộng API Scala bằng cách sử dụng các tính năng lập trình cấp kiểu khác nhau. Đó là một dự án rất thú vị có một lượng lớn người theo dõi.
MetaScala là một thư viện cấp kiểu cho Scala, bao gồm các kiểu meta cho số tự nhiên, boolean, đơn vị, HList, v.v. Đây là một dự án của Jesper Nordenberg (blog của anh ấy) .
Các Michid (blog) có một số ví dụ tuyệt vời của chương trình loại cấp tại Scala (từ câu trả lời khác):
Debasish Ghosh (blog) cũng có một số bài đăng liên quan:
(Tôi đã thực hiện một số nghiên cứu về chủ đề này và đây là những gì tôi đã học được. Tôi vẫn chưa quen với nó, vì vậy vui lòng chỉ ra bất kỳ điểm nào không chính xác trong câu trả lời này.)