Có hai câu hỏi riêng biệt ở đây:
- Tại sao sử dụng hình thù loại thành viên thay vì các tham số kiểu trong một số trường hợp trong một số các lớp học kiểu?
- Tại sao Shapeless bao gồm
Aux
bí danh loại trong đối tượng đồng hành của các lớp loại này?
Tôi sẽ bắt đầu với câu hỏi thứ hai vì câu trả lời đơn giản hơn: các bí danh loại Aux
hoàn toàn là một cú pháp tiện lợi. Bạn không bao giờ có để sử dụng chúng. Ví dụ, giả sử chúng ta muốn viết một phương pháp mà chỉ sẽ biên dịch khi được gọi với hai hlists có cùng chiều dài:
import shapeless._, ops.hlist.Length
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length.Aux[A, N],
bl: Length.Aux[B, N]
) =()
Lớp Length
loại có một tham số kiểu (đối với loại HList
) và một thành viên loại (cho số Nat
). Cú pháp Length.Aux
làm cho nó tương đối dễ dàng để tham khảo các Nat
loại thành viên trong danh sách tham số ẩn, nhưng nó chỉ là một sự tiện lợi-sau chính xác là tương đương:
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length[A] { type Out = N },
bl: Length[B] { type Out = N }
) =()
Phiên bản Aux
có một vài ưu điểm so với viết ra các loại sàng lọc theo cách này: nó ít ồn hơn, và nó không yêu cầu chúng ta nhớ tên của thành viên loại. Tuy nhiên, đây là những vấn đề về ergonomic hoàn toàn, các bí danh Aux
làm cho mã của chúng tôi dễ đọc và dễ viết hơn, nhưng chúng không thay đổi những gì chúng ta có thể hoặc không thể làm với mã theo bất kỳ cách nào có ý nghĩa.
Câu trả lời cho câu hỏi đầu tiên phức tạp hơn một chút. Trong nhiều trường hợp, bao gồm cả sameLength
của tôi, không có lợi thế nào để Out
là một thành viên loại thay vì tham số loại. Vì Scala doesn't allow multiple implicit parameter sections, chúng tôi cần N
để trở thành thông số loại cho phương pháp của chúng tôi nếu chúng tôi muốn xác minh rằng hai trường hợp Length
có cùng loại Out
. Tại thời điểm đó, số Out
trên Length
cũng có thể là thông số loại (ít nhất là từ quan điểm của chúng tôi với tư cách là tác giả của sameLength
).
Trong trường hợp khác, chúng ta có thể tận dụng thực tế là Shapeless đôi khi (tôi sẽ nói cụ thể trong đó trong một thời điểm) sử dụng loại thành viên thay vì các thông số loại. Ví dụ, giả sử chúng ta muốn viết một phương pháp mà sẽ trả về một chức năng mà sẽ chuyển đổi một loại trường hợp lớp được chỉ định vào một HList
:
def converter[A](implicit gen: Generic[A]): A => gen.Repr = a => gen.to(a)
Bây giờ chúng ta có thể sử dụng nó như thế này:
case class Foo(i: Int, s: String)
val fooToHList = converter[Foo]
Và chúng tôi sẽ có được Foo => Int :: String :: HNil
. Nếu Generic
's Repr
được một tham số kiểu thay vì một thành viên loại, chúng tôi phải viết một cái gì đó như thế này thay vì:
// Doesn't compile
def converter[A, R](implicit gen: Generic[A, R]): A => R = a => gen.to(a)
Scala không hỗ trợ ứng dụng một phần của tham số kiểu, vì vậy mỗi lần chúng tôi gọi đây là phương pháp (giả), chúng tôi sẽ phải xác định cả hai tham số kiểu vì chúng ta muốn xác định A
:
val fooToHList = converter[Foo, Int :: String :: HNil]
Điều này làm cho nó về cơ bản vô dụng, vì toàn bộ vấn đề là để cho các nhân vật máy móc generic ra đại diện.
Nói chung, bất cứ khi nào một kiểu được xác định duy nhất bởi các tham số khác của một loại lớp, Shapeless sẽ biến nó thành một thành viên kiểu thay vì tham số kiểu. Mỗi trường hợp lớp có một đại diện chung duy nhất, vì vậy Generic
có một loại tham số (đối với loại trường hợp loại) và một loại thành viên (đối với loại đại diện); mỗi HList
có một chiều dài duy nhất, vì vậy Length
có một thông số loại và một loại thành viên, v.v.
Thực hiện các loại kiểu xác định duy nhất thay vì thông số loại có nghĩa là chúng ta chỉ sử dụng chúng làm loại phụ thuộc vào đường dẫn (như trong số converter
đầu tiên ở trên), chúng ta có thể, nhưng nếu chúng ta muốn sử dụng chúng như thể chúng là các tham số kiểu, chúng ta luôn có thể viết kiểu sàng lọc (hoặc phiên bản Aux
tốt hơn về cú pháp). Nếu Shapeless thực hiện các kiểu tham số này ngay từ đầu, nó sẽ không thể đi theo hướng ngược lại.
Là một mặt lưu ý, mối quan hệ này giữa loại "thông số", một kiểu lớp của (Tôi đang sử dụng dấu ngoặc kép vì chúng có thể không thông số theo nghĩa Scala đen) được gọi là một "functional dependency" các ngôn ngữ như Haskell, nhưng bạn không nên cảm thấy như bạn cần phải hiểu bất cứ điều gì về phụ thuộc chức năng trong Haskell để có được những gì đang xảy ra trong Shapeless.