Tài nguyên lập trình kiểu Scala


102

Theo câu hỏi này , hệ thống loại của Scala là Turing hoàn chỉnh . Những tài nguyên nào có sẵn cho phép một người mới sử dụng tận dụng sức mạnh của lập trình cấp kiểu?

Đây là những tài nguyên mà tôi đã tìm thấy cho đến nay:

Những tài nguyên này rất tuyệt, nhưng tôi cảm thấy như mình đang thiếu những điều cơ bản và vì vậy không có một nền tảng vững chắc để xây dựng. Ví dụ, ở đâu có phần giới thiệu về định nghĩa kiểu? Tôi có thể thực hiện những thao tác nào trên các loại?

Có bất kỳ nguồn giới thiệu tốt nào không?


Cá nhân tôi thấy giả định rằng một người muốn lập trình mức kiểu trong Scala đã biết cách lập trình trong Scala là khá hợp lý. Thậm chí nếu nó có nghĩa là tôi không hiểu một từ trong những bài viết bạn liên quan đến :-)
Jörg W Mittag

Câu trả lời:


140

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 ( ST, cả hai đều phải là kiểu con của Lambda), được trait Lam extends Lambdatham 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 Appcủ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 evalkiểu tham số của đặc điểm Svà gọi applyvới tham số Ttrên kết quả. Lưu ý, Sđược đảm bảo có một evalkiểu vì tham số chỉ định nó là một kiểu con của Lambda. Tương tự, kết quả của evalphải có một applykiể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 Alà một kiểu con củaB
      • A =:= B, chỉ biên dịch nếu Alà một kiểu con của BBlà một kiểu con củaA
      • A <%< B, ("có thể xem dưới dạng") chỉ biên dịch khi Acó thể xem dưới dạng B(tức là có một chuyển đổi ngầm định từ Athà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 nulllà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 Alà 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ừ metascalaapocalisp ):

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 đó Akiể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 vallà 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 Natvà trả về một số nguyên đại diện cho số tự nhiên được mã hóa trong vkiểu của. Ví dụ: nếu chúng ta có giá trị val x:_3 = null( nullkiể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 TypeToValuecho mỗi Nattừ _0lê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: TVT. Ttươ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à VTtươ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 toIntnhư sau:

def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()

Để hiểu cách toInthoạ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 ttvvề kiểu TypeToValue[_0, Int](vì zlà kiểu _0). Nó tìm thấy đối tượng _0ToInt, nó gọi getValuephươ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 ttvvề kiểu TypeToValue[Succ[_0], Int](vì ylà 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à, TypeToValuetham 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à succToIntxâ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 AB, mà bạn muốn xác minh là bằng nhau. Sau đó, kiểm tra xem biên dịch sau:

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 alà loại Abthuộ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.)


12

Chỉ muốn nói lời cảm ơn vì blog thú vị; Tôi đã theo dõi nó một thời gian và đặc biệt là bài đăng cuối cùng được đề cập ở trên đã giúp tôi hiểu rõ hơn về các thuộc tính quan trọng mà một hệ thống kiểu cho một ngôn ngữ hướng đối tượng cần phải có. Vì vậy, cảm ơn!
Zach Snow



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.