2015-12-31 13 views
40

Tôi khá chắc chắn rằng tôi đang thiếu một cái gì đó ở đây, vì tôi là khá mới để Shapeless và tôi đang học, nhưng khi là kỹ thuật Aux thực sự yêu cầu? Tôi thấy rằng nó được sử dụng để hiển thị tuyên bố type bằng cách tăng nó lên chữ ký của một định nghĩa "đồng hành" khác type.Tại sao kỹ thuật Aux yêu cầu cho tính toán mức loại?

trait F[A] { type R; def value: R } 
object F { type Aux[A,RR] = F[A] { type R = RR } } 

nhưng điều này gần như tương đương với việc đặt R vào chữ ký loại F?

trait F[A,R] { def value: R } 
implicit def fint = new F[Int,Long] { val value = 1L } 
implicit def ffloat = new F[Float,Double] { val value = 2.0D } 
def f[T,R](t:T)(implicit f: F[T,R]): R = f.value 
f(100) // res4: Long = 1L 
f(100.0f) // res5: Double = 2.0 

Tôi thấy rằng loại đường phụ thuộc sẽ mang lại lợi ích nếu người ta có thể sử dụng chúng trong danh sách đối số, nhưng chúng tôi biết chúng tôi không thể làm

def g[T](t:T)(implicit f: F[T], r: Blah[f.R]) ... 

do đó, chúng tôi vẫn buộc phải đặt một tham số kiểu bổ sung trong chữ ký của g. Bằng cách sử dụng kỹ thuật Aux, chúng tôi cũng cũng cần để dành thêm thời gian để viết đồng hành object. Từ quan điểm sử dụng, nó sẽ xem xét đến một người dùng ngây thơ như tôi rằng không có lợi ích gì khi sử dụng các kiểu phụ thuộc vào đường dẫn.

Chỉ có một trường hợp tôi có thể nghĩ, nghĩa là, đối với một phép tính mức loại nhất định hơn một kết quả loại cấp được trả lại và bạn có thể chỉ muốn sử dụng một trong số chúng.

Tôi đoán tất cả sẽ tóm tắt với tôi khi nhìn thấy điều gì đó trong ví dụ đơn giản của tôi.

Trả lời

49

Có hai câu hỏi riêng biệt ở đây:

  1. 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?
  2. 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ờ để 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.

Các vấn đề liên quan