Có phải những người theo chủ nghĩa không có gì khác hơn là một cách viết lộn xộn?


144

Tôi thực sự quan tâm đến việc tìm ra sự khác biệt ở đâu, và nói chung, để xác định các trường hợp sử dụng kinh điển trong đó không thể sử dụng HLists (hay nói đúng hơn là không mang lại bất kỳ lợi ích nào trong danh sách thông thường).

(Tôi biết rằng có 22 (tôi tin) TupleNở Scala, trong khi người ta chỉ cần một HList duy nhất, nhưng đó không phải là loại khác biệt về khái niệm mà tôi quan tâm.)

Tôi đã đánh dấu một vài câu hỏi trong văn bản dưới đây. Có thể không thực sự cần thiết phải trả lời chúng, chúng có ý nghĩa hơn để chỉ ra những điều không rõ ràng với tôi và hướng dẫn cuộc thảo luận theo những hướng nhất định.

Động lực

Gần đây tôi đã thấy một vài câu trả lời trên SO nơi mọi người đề xuất sử dụng HLists (ví dụ, được cung cấp bởi Shapless ), bao gồm cả câu trả lời đã bị xóa cho câu hỏi này . Nó đã dẫn đến cuộc thảo luận này , từ đó đặt ra câu hỏi này.

Giới thiệu

Dường như với tôi, danh sách đó chỉ hữu ích khi bạn biết số lượng phần tử và loại chính xác của chúng một cách tĩnh. Con số thực sự không quan trọng, nhưng có vẻ như bạn không cần phải tạo một danh sách với các yếu tố khác nhau nhưng được biết chính xác về mặt tĩnh, nhưng bạn không biết rõ về số của chúng. Câu hỏi 1: Bạn thậm chí có thể viết một ví dụ như vậy, ví dụ, trong một vòng lặp? Trực giác của tôi là việc có một danh sách chính xác tĩnh với số lượng phần tử tùy ý không xác định (tùy ý liên quan đến hệ thống phân cấp lớp nhất định) không tương thích.

HLists so với Tuples

Nếu điều này là đúng, tức là, bạn hoàn toàn biết số và loại - Câu hỏi 2: tại sao không chỉ sử dụng một n-tuple? Chắc chắn, bạn có thể lập bản đồ một cách dễ dàng và gấp lại một HList (mà bạn cũng có thể, nhưng không phải là một cách dễ dàng, thực hiện trên một tuple với sự trợ giúp của productIterator), nhưng vì số lượng và loại của các phần tử được biết đến một cách tĩnh mà bạn có thể chỉ cần truy cập vào các phần tử tuple trực tiếp và thực hiện các hoạt động.

Mặt khác, nếu chức năng fbạn ánh xạ trên một danh sách chung chung đến mức nó chấp nhận tất cả các yếu tố - Câu hỏi 3: tại sao không sử dụng nó thông qua productIterator.map? Ok, một sự khác biệt thú vị có thể đến từ quá tải phương thức: nếu chúng ta có một số quá tải f, việc có thông tin loại mạnh hơn được cung cấp bởi danh sách (ngược lại với ProductIterator) có thể cho phép trình biên dịch chọn cụ thể hơn f. Tuy nhiên, tôi không chắc liệu điều đó có thực sự hoạt động trong Scala hay không, vì các phương thức và chức năng không giống nhau.

HLists và đầu vào của người dùng

Dựa trên cùng một giả định, cụ thể là bạn cần biết số lượng và loại yếu tố một cách tĩnh - Câu hỏi 4: danh sách có thể được sử dụng trong các tình huống mà các yếu tố phụ thuộc vào bất kỳ loại tương tác người dùng nào không? Ví dụ, hãy tưởng tượng điền vào một danh sách với các phần tử bên trong một vòng lặp; các yếu tố được đọc từ đâu đó (UI, tệp cấu hình, tương tác diễn viên, mạng) cho đến khi một điều kiện nhất định được giữ. Loại của danh sách sẽ là gì? Tương tự đối với một đặc tả giao diện getElements: HList [...] sẽ hoạt động với các danh sách có độ dài không xác định tĩnh và cho phép thành phần A trong hệ thống có được danh sách các phần tử tùy ý từ thành phần B.

Câu trả lời:


144

Giải quyết các câu hỏi từ một đến ba: một trong những ứng dụng chính cho việc HListstrừu tượng hóa arity. Arity thường được biết đến tĩnh tại bất kỳ trang web sử dụng nhất định nào về sự trừu tượng, nhưng khác nhau tùy theo từng trang web. Lấy điều này, từ các ví dụ của shapless ,

def flatten[T <: Product, L <: HList](t : T)
  (implicit hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =
    flatten(hl(t))

val t1 = (1, ((2, 3), 4))
val f1 = flatten(t1)     // Inferred type is Int :: Int :: Int :: Int :: HNil
val l1 = f1.toList       // Inferred type is List[Int]

val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
// Inferred type of t2b is (Int, Boolean, Double, String, String, Int, Boolean)

Nếu không sử dụng HLists(hoặc một cái gì đó tương đương) để trừu tượng hóa tính không hợp lý của các đối số tuple flattenthì sẽ không thể có một triển khai duy nhất có thể chấp nhận các đối số của hai hình dạng rất khác nhau này và biến đổi chúng theo cách an toàn.

Khả năng trừu tượng hóa arity có thể được quan tâm ở bất cứ nơi nào có liên quan đến các arent cố định: cũng như các bộ dữ liệu, như trên, bao gồm danh sách tham số phương thức / hàm và các lớp trường hợp. Xem ở đây để biết ví dụ về cách chúng ta có thể trừu tượng hóa tính chất của các lớp trường hợp tùy ý để có được các thể hiện của lớp loại gần như tự động,

// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)

// Publish their `HListIso`'s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)

// And now they're monoids ...

implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))

implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))

Không có lặp lại thời gian chạy ở đây, nhưng có sự trùng lặp , mà việc sử dụng HLists(hoặc các cấu trúc tương đương) có thể loại bỏ. Tất nhiên, nếu khả năng chịu đựng của bạn đối với nồi hơi lặp đi lặp lại cao, bạn có thể nhận được kết quả tương tự bằng cách viết nhiều triển khai cho mỗi và mọi hình dạng mà bạn quan tâm.

Trong câu hỏi ba bạn hỏi "... nếu hàm f bạn ánh xạ trên một danh sách chung chung đến mức nó chấp nhận tất cả các yếu tố ... tại sao không sử dụng nó thông qua sản phẩmIterator.map?". Nếu chức năng bạn ánh xạ qua một HList thực sự có dạng Any => Tthì ánh xạ qua productIteratorsẽ phục vụ bạn hoàn toàn tốt. Nhưng các chức năng của biểu mẫu Any => Tthường không thú vị (ít nhất, chúng không trừ khi chúng nhập nội bộ). shapless cung cấp một dạng giá trị hàm đa hình cho phép trình biên dịch chọn các trường hợp cụ thể theo kiểu chính xác theo cách bạn nghi ngờ. Ví dụ,

// size is a function from values of arbitrary type to a 'size' which is
// defined via type specific cases
object size extends Poly1 {
  implicit def default[T] = at[T](t => 1)
  implicit def caseString = at[String](_.length)
  implicit def caseList[T] = at[List[T]](_.length)
}

scala> val l = 23 :: "foo" :: List('a', 'b') :: true :: HNil
l: Int :: String :: List[Char] :: Boolean :: HNil =
  23 :: foo :: List(a, b) :: true :: HNil

scala> (l map size).toList
res1: List[Int] = List(1, 3, 2, 1)

Đối với câu hỏi của bạn bốn, về đầu vào của người dùng, có hai trường hợp cần xem xét. Đầu tiên là các tình huống trong đó chúng ta có thể tự động thiết lập một bối cảnh đảm bảo rằng một điều kiện tĩnh đã biết có được. Trong các loại kịch bản này, hoàn toàn có thể áp dụng các kỹ thuật tạo hình, nhưng rõ ràng với điều kiện là nếu điều kiện tĩnh không có được trong thời gian chạy thì chúng ta phải đi theo một con đường khác. Không có gì đáng ngạc nhiên, điều này có nghĩa là các phương pháp nhạy cảm với điều kiện động phải mang lại kết quả tùy chọn. Đây là một ví dụ sử dụng HLists,

trait Fruit
case class Apple() extends Fruit
case class Pear() extends Fruit

type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil

val a : Apple = Apple()
val p : Pear = Pear()

val l = List(a, p, a, p) // Inferred type is List[Fruit]

Loại lkhông nắm bắt được độ dài của danh sách hoặc các loại chính xác của các yếu tố. Tuy nhiên, nếu chúng ta hy vọng nó có một hình thức cụ thể (nghĩa là nếu nó phải phù hợp với một số lược đồ cố định đã biết), thì chúng ta có thể cố gắng thiết lập thực tế đó và hành động tương ứng,

scala> import Traversables._
import Traversables._

scala> val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]
res0: Option[Apple :: Pear :: Apple :: Pear :: HNil] =
  Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)

scala> apap.map(_.tail.head)
res1: Option[Pear] = Some(Pear())

Có những tình huống khác mà chúng ta có thể không quan tâm đến độ dài thực tế của một danh sách nhất định, ngoài ra nó có cùng độ dài với một số danh sách khác. Một lần nữa, đây là thứ mà shapless hỗ trợ, cả hoàn toàn tĩnh, và trong bối cảnh tĩnh / động hỗn hợp như trên. Xem ở đây cho một ví dụ mở rộng.

Đúng như bạn quan sát, tất cả các cơ chế này đều yêu cầu thông tin loại tĩnh, ít nhất là có điều kiện, và dường như sẽ loại trừ các kỹ thuật này khỏi khả năng sử dụng trong môi trường hoàn toàn động, được điều khiển hoàn toàn bởi dữ liệu được cung cấp bên ngoài. Nhưng với sự ra đời của hỗ trợ biên dịch thời gian chạy như là một thành phần của phản xạ Scala trong 2.10, thậm chí đây không còn là trở ngại không thể khắc phục ... chúng ta có thể sử dụng biên dịch thời gian chạy để cung cấp một hình thức dàn dựng nhẹ và thực hiện gõ tĩnh để đáp ứng với dữ liệu động: trích đoạn trước dưới đây ... hãy theo liên kết để biết ví dụ đầy đủ,

val t1 : (Any, Any) = (23, "foo") // Specific element types erased
val t2 : (Any, Any) = (true, 2.0) // Specific element types erased

// Type class instances selected on static type at runtime!
val c1 = stagedConsumeTuple(t1) // Uses intString instance
assert(c1 == "23foo")

val c2 = stagedConsumeTuple(t2) // Uses booleanDouble instance
assert(c2 == "+2.0")

Tôi chắc chắn @PLT_Borat sẽ có điều gì đó để nói về điều đó, đưa ra những nhận xét hiền triết của anh ấy về các ngôn ngữ lập trình được gõ phụ thuộc ;-)


2
Tôi hơi bối rối bởi phần cuối của câu trả lời của bạn - nhưng cũng rất hấp dẫn! Cảm ơn câu trả lời tuyệt vời của bạn và nhiều tài liệu tham khảo, có vẻ như tôi có rất nhiều việc phải đọc :-)
Malte Schwerhoff

1
Trừu tượng hóa trên arity là vô cùng hữu ích. Đáng buồn thay, ScalaMock phải chịu sự trùng lặp đáng kể vì những FunctionNđặc điểm khác nhau không biết cách trừu tượng hóa arity: github.com/paulbutcher/ScalaMock/blob/develop/core/src/main/ tựa github.com/paulbutcher/ScalaMock/blo / phát triển / lõi / src / main / ... Đáng buồn là tôi không biết cách nào mà tôi có thể sử dụng hình thù để tránh điều này, cho rằng tôi cần phải đối phó với "thực" FunctionNs
Paul Butcher

1
Tôi đã tạo ra một ví dụ (khá giả tạo) - ideone.com/sxIw1 -, nằm dọc theo dòng câu hỏi thứ nhất. Điều này có thể được hưởng lợi từ danh sách, có thể kết hợp với "gõ tĩnh được thực hiện trong thời gian chạy để đáp ứng với dữ liệu động"? (Tôi vẫn không chắc chắn chính xác cái thứ hai nói về cái gì)
Malte Schwerhoff

17

Nói rõ hơn, một HList về cơ bản không có gì khác hơn là một chồng Tuple2với đường hơi khác nhau ở trên.

def hcons[A,B](head : A, tail : B) = (a,b)
def hnil = Unit

hcons("foo", hcons(3, hnil)) : (String, (Int, Unit))

Vì vậy, câu hỏi của bạn về cơ bản là về sự khác biệt giữa việc sử dụng các tuple lồng nhau và các tuple phẳng, nhưng hai cái này là đẳng cấu nên cuối cùng thực sự không có sự khác biệt nào ngoài sự tiện lợi trong đó có thể sử dụng các chức năng thư viện và sử dụng ký hiệu nào.


các bộ dữ liệu có thể được ánh xạ vào danh sách và dù sao đi nữa, do đó, có một sự đồng hình rõ ràng.
Erik Kaplun

10

Có rất nhiều điều bạn không thể làm (tốt) với các bộ dữ liệu:

  • viết một hàm tổng hợp / bổ sung chung
  • viết hàm ngược
  • viết hàm concat
  • ...

Tất nhiên bạn có thể làm tất cả điều đó với bộ dữ liệu, nhưng không phải trong trường hợp chung. Vì vậy, sử dụng HLists làm cho mã của bạn DRY hơn.


8

Tôi có thể giải thích điều này bằng ngôn ngữ siêu đơn giản:

Việc đặt tên so với danh sách không đáng kể. HLists có thể được đặt tên là HTuples. Sự khác biệt là trong Scala + Haskell, bạn có thể thực hiện việc này bằng một tuple (sử dụng cú pháp Scala):

def append2[A,B,C](in: (A,B), v: C) : (A,B,C) = (in._1, in._2, v)

để lấy một bộ dữ liệu đầu vào gồm hai phần tử thuộc bất kỳ loại nào, nối thêm phần tử thứ ba và trả về một bộ dữ liệu được nhập đầy đủ với chính xác ba phần tử. Nhưng trong khi điều này là hoàn toàn chung chung về các loại, nó phải xác định rõ ràng độ dài đầu vào / đầu ra.

Điều mà một người theo phong cách Haskell cho phép bạn làm là tạo ra cái chung này theo chiều dài, do đó bạn có thể nối thêm bất kỳ độ dài nào của danh sách / danh sách và lấy lại một bộ / danh sách được gõ hoàn toàn tĩnh. Lợi ích này cũng áp dụng cho các bộ sưu tập được gõ đồng nhất, trong đó bạn có thể nối một int vào danh sách chính xác n ints và lấy lại danh sách được nhập tĩnh để có int (n + 1) chính xác mà không cần chỉ định rõ ràng n.

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.