HLists ไม่มีอะไรมากไปกว่าการเขียนสิ่งที่ซับซ้อน


144

ฉันสนใจที่จะค้นหาว่ามีความแตกต่างอยู่ที่ใดและโดยทั่วไปแล้วเพื่อระบุกรณีการใช้งานที่เป็นที่ยอมรับซึ่ง HLists ไม่สามารถใช้งานได้ (หรือมากกว่าไม่ให้ผลประโยชน์ใด ๆ กับรายการปกติ)

(ฉันรู้ว่ามี 22 (ฉันเชื่อว่า) TupleNในสกาล่าในขณะที่หนึ่งต้องการเพียง HList เดียว แต่นั่นไม่ใช่ความแตกต่างทางแนวคิดที่ฉันสนใจ)

ฉันได้ทำเครื่องหมายคำถามสองสามข้อในข้อความด้านล่าง อาจไม่จำเป็นที่จะต้องตอบคำถามพวกเขามีจุดมุ่งหมายที่จะชี้ให้เห็นสิ่งที่ไม่ชัดเจนสำหรับฉันและเพื่อเป็นแนวทางในการสนทนาในบางทิศทาง

แรงจูงใจ

เมื่อเร็ว ๆ นี้ฉันเคยเห็นคำตอบสองสามข้อเกี่ยวกับ SO ที่ผู้คนแนะนำให้ใช้ HLists (เช่นที่จัดทำโดยShapeless ) รวมถึงคำตอบที่ถูกลบสำหรับคำถามนี้ มันก่อให้เกิดการสนทนานี้ซึ่งจะจุดประกายคำถามนี้

Intro

ดูเหมือนว่าสำหรับฉันแล้ว hlists นั้นจะมีประโยชน์ก็ต่อเมื่อคุณรู้จำนวนองค์ประกอบและประเภทที่แม่นยำเท่านั้น จำนวนนั้นไม่สำคัญ แต่ดูเหมือนไม่น่าเป็นไปได้ที่คุณจะต้องสร้างรายการที่มีองค์ประกอบของชนิดที่แตกต่างกัน แต่เป็นที่รู้จักอย่างแน่นอนในแบบสแตติก แต่คุณไม่รู้จักหมายเลขของพวกเขาแบบคงที่ คำถามที่ 1:คุณสามารถเขียนตัวอย่างเช่นในวงได้หรือไม่? สัญชาตญาณของฉันคือการมี hlister ที่แม่นยำแบบคงที่พร้อมด้วยจำนวนองค์ประกอบที่ไม่รู้จักแบบคงที่ (โดยพลการสัมพันธ์กับลำดับชั้นของชั้นเรียนที่กำหนด) ไม่เข้ากัน

HLists vs. Tuples

ถ้านี่เป็นเรื่องจริงนั่นคือคุณรู้จำนวนและประเภทของคำถาม - คำถามที่ 2:ทำไมไม่ใช้ n-tuple? แน่นอนว่าคุณสามารถทำแผนที่และพับ HList ได้อย่างปลอดภัย (ซึ่งคุณสามารถทำได้ แต่ไม่สามารถพิมพ์ได้อย่างปลอดภัยโดยใช้ความช่วยเหลือของ tuple productIterator) แต่เนื่องจากทราบว่าหมายเลขและประเภทขององค์ประกอบเป็นแบบคงที่คุณอาจเข้าถึงองค์ประกอบ tuple โดยตรงและดำเนินการ

ในทางกลับกันถ้าฟังก์ชั่นที่fคุณจับคู่กับ hlist นั้นเป็นเรื่องทั่วไปที่ยอมรับองค์ประกอบทั้งหมด - คำถามที่ 3:ทำไมไม่ใช้มันผ่านproductIterator.map? ตกลงหนึ่งความแตกต่างที่น่าสนใจอาจจะมาจากวิธีการบรรทุกเกินพิกัด: ถ้าเราได้มากเกินไปหลายf's มีข้อมูลชนิดแข็งแกร่งให้โดย hlist (ตรงกันข้ามกับ productIterator) fที่อาจทำให้คอมไพเลอร์จะเลือกเฉพาะเจาะจงมากขึ้น อย่างไรก็ตามฉันไม่แน่ใจว่าจะใช้งานได้จริงใน Scala หรือไม่เนื่องจากวิธีการและฟังก์ชั่นนั้นไม่เหมือนกัน

HLists และอินพุตของผู้ใช้

การสร้างข้อสมมติเดียวกันคือคุณจำเป็นต้องรู้จำนวนและประเภทขององค์ประกอบแบบคงที่ - คำถามที่ 4: hlists สามารถใช้ในสถานการณ์ที่องค์ประกอบขึ้นอยู่กับการโต้ตอบของผู้ใช้หรือไม่? เช่นจินตนาการการเติม hlist ด้วยองค์ประกอบภายในลูป องค์ประกอบจะอ่านจากที่ใดที่หนึ่ง (UI, ไฟล์กำหนดค่า, การโต้ตอบของนักแสดง, เครือข่าย) จนกว่าจะมีเงื่อนไขบางประการ ประเภทของ hlist คืออะไร? คล้ายกับข้อกำหนดคุณลักษณะอินเตอร์เฟส getElements: HList [... ] ที่ควรทำงานกับรายการความยาวที่ไม่รู้จักแบบคงที่และอนุญาตให้คอมโพเนนต์ A ในระบบได้รับรายการองค์ประกอบใด ๆ จากองค์ประกอบ B

คำตอบ:


144

การตอบคำถามหนึ่งถึงสาม: แอปพลิเคชันหลักอย่างหนึ่งสำหรับการHListsสรุปที่เป็นนามธรรม โดยทั่วไปแล้ว Arity นั้นจะรู้จักกันในไซต์การใช้งานที่เป็นนามธรรม แต่จะแตกต่างกันไปตามแต่ละไซต์ เวลานี้จากไม่มีรูปแบบของตัวอย่าง ,

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)

โดยไม่ต้องใช้HLists(หรือสิ่งที่เทียบเท่า) เพื่อนามธรรมเหนือ arity ของอาร์กิวเมนต์ tuple flattenมันจะเป็นไปไม่ได้ที่จะมีการใช้งานเพียงครั้งเดียวซึ่งสามารถยอมรับข้อโต้แย้งของทั้งสองรูปร่างที่แตกต่างกันมากและเปลี่ยนพวกเขาในวิธีที่ปลอดภัยประเภท

ความสามารถในการนามธรรมเหนือ arity น่าจะเป็นที่สนใจทุกที่ที่ arities คงที่เกี่ยวข้อง: เช่นเดียวกับ tuples ดังกล่าวข้างต้นที่มีรายการพารามิเตอร์วิธี / ฟังก์ชั่นและชั้นเรียนกรณี ดูที่นี่สำหรับตัวอย่างของวิธีที่เราอาจสรุปความ arity ของคลาสเคสที่กำหนดเองเพื่อรับอินสแตนซ์คลาสของคลาสโดยอัตโนมัติ

// 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))

ไม่มีการวนซ้ำแบบรันไทม์ที่นี่ แต่มีการทำซ้ำซึ่งการใช้HLists(หรือโครงสร้างที่เทียบเท่า) สามารถกำจัดได้ แน่นอนถ้าความอดทนของคุณสำหรับหม้อไอน้ำซ้ำ ๆ สูงคุณสามารถได้รับผลลัพธ์เดียวกันโดยการเขียนการใช้งานที่หลากหลายสำหรับแต่ละรูปร่างที่คุณสนใจ

ในคำถามที่สามคุณถามว่า "... ถ้าฟังก์ชั่น f ที่คุณจับคู่กับ hlist นั้นสามัญมากจนยอมรับองค์ประกอบทั้งหมด ... ทำไมไม่ใช้มันผ่าน productIterator.map?" หากฟังก์ชั่นที่คุณแมปผ่าน HList นั้นเป็นแบบฟอร์มจริงๆAny => Tแล้วการจับคู่ผ่านproductIteratorจะให้บริการคุณได้อย่างสมบูรณ์แบบ แต่Any => Tโดยทั่วไปแล้วฟังก์ชั่นของแบบฟอร์มนั้นไม่น่าสนใจ (อย่างน้อยพวกมันก็ไม่ได้เว้นแต่ว่าพวกเขาจะพิมพ์ภายใน) shapeless ให้รูปแบบของค่าฟังก์ชัน polymorphic ซึ่งทำให้คอมไพเลอร์สามารถเลือกเคสเฉพาะประเภทในแบบที่คุณสงสัย ตัวอย่างเช่น

// 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)

สำหรับคำถามที่สี่เกี่ยวกับการป้อนข้อมูลของผู้ใช้มีสองกรณีที่ต้องพิจารณา สิ่งแรกคือสถานการณ์ที่เราสามารถสร้างบริบทซึ่งรับประกันได้ว่าเงื่อนไขแบบสแตติกที่รู้จักนั้นจะได้รับ ในสถานการณ์ประเภทนี้เป็นไปได้อย่างสมบูรณ์แบบที่จะใช้เทคนิคที่ไม่มีรูปแบบ แต่เห็นได้ชัดว่ามีเงื่อนไขว่าหากเงื่อนไขแบบสแตติกไม่ได้มาที่รันไทม์ดังนั้นเราต้องทำตามเส้นทางอื่น ไม่น่าแปลกใจซึ่งหมายความว่าวิธีการที่มีความไวต่อเงื่อนไขแบบไดนามิกต้องให้ผลลัพธ์ที่เป็นตัวเลือก นี่คือตัวอย่างการใช้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]

ประเภทของlไม่จับภาพความยาวของรายการหรือประเภทองค์ประกอบที่แม่นยำ อย่างไรก็ตามถ้าเราคาดหวังว่ามันจะมีรูปแบบเฉพาะ (เช่นถ้ามันควรจะสอดคล้องกับที่รู้จักกันบางสคีคงที่) จากนั้นเราสามารถพยายามที่จะสร้างความจริงนั้นและดำเนินการตามนั้น

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())

มีสถานการณ์อื่น ๆ ที่เราอาจไม่สนใจเกี่ยวกับความยาวจริงของรายการที่กำหนดนอกเหนือจากนั้นจะมีความยาวเท่ากับรายการอื่น ๆ อีกครั้งนี่คือสิ่งที่ไม่มีรูปแบบรองรับทั้งแบบสแตติกและในบริบทแบบสแตติก / ไดนามิกดังกล่าวข้างต้น ดูที่นี่สำหรับตัวอย่างเพิ่มเติม

มันเป็นความจริงในขณะที่คุณสังเกตเห็นว่ากลไกเหล่านี้ทั้งหมดต้องการข้อมูลประเภทสแตติกเพื่อให้สามารถใช้ได้อย่างน้อยมีเงื่อนไขและดูเหมือนว่าจะแยกเทคนิคเหล่านี้ออกจากการใช้งานในสภาพแวดล้อมแบบไดนามิกสมบูรณ์ขับเคลื่อนโดยข้อมูลที่ไม่ได้พิมพ์ แต่ด้วยการถือกำเนิดของการสนับสนุนสำหรับการรวบรวมรันไทม์เป็นส่วนประกอบของ Scala สะท้อนใน 2.10 ที่แม้นี้ไม่เป็นอุปสรรคผ่านไปไม่ได้ ... เราสามารถใช้การรวบรวมรันไทม์เพื่อให้รูปแบบของการแสดงละครที่มีน้ำหนักเบาและมีการพิมพ์แบบคงที่เราดำเนินการที่รันไทม์ ในการตอบสนองต่อข้อมูลแบบไดนามิก: ข้อความที่ตัดตอนมาจากด้านล่างก่อนหน้า ... ไปตามลิงก์สำหรับตัวอย่างเต็ม

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")

ฉันแน่ใจว่า@PLT_Boratจะมีบางอย่างที่จะพูดเกี่ยวกับเรื่องนี้เนื่องจากความเห็นของปราชญ์เกี่ยวกับภาษาการเขียนโปรแกรมที่พึ่งพาได้ ;-)


2
ฉันสับสนเล็กน้อยในคำตอบสุดท้ายของคุณ - แต่ก็ทึ่งมาก! ขอบคุณสำหรับคำตอบที่ดีของคุณและการอ้างอิงหลายลักษณะเช่นถ้าฉันได้มากในการอ่านที่จะทำ :-)
Malte Schwerhoff

1
การสรุปเรื่อง arity มีประโยชน์อย่างยิ่ง ScalaMock น่าเศร้าทนทุกข์ทรมานจากการทำซ้ำมากเพราะFunctionNลักษณะต่าง ๆไม่ทราบวิธีการนามธรรมเหนือ arity: github.com/paulbutcher/ScalaMock/blob/develop/core/src/main/… github.com/paulbutcher/ScalaMock/blob / พัฒนา / core / src / main / ... น่าเศร้าที่ฉันไม่ได้ตระหนักถึงวิธีการใด ๆ ที่ฉันสามารถใช้ไม่มีรูปแบบที่จะหลีกเลี่ยงปัญหานี้ให้ที่ฉันต้องการที่จะจัดการกับ "ของจริง" FunctionNs
พอลบุชเชอร์

1
ฉันสร้างตัวอย่าง (ประดิษฐ์ขึ้นมาสวย ๆ ) - ideone.com/sxIw1 - ซึ่งเรียงตามแนวคำถามหนึ่ง สิ่งนี้จะได้ประโยชน์จาก hlists หรืออาจใช้ร่วมกับ "การพิมพ์แบบคงที่ที่รันไทม์เพื่อตอบสนองต่อข้อมูลแบบไดนามิก" (ฉันยังไม่แน่ใจว่าสิ่งที่เกี่ยวกับหลัง)
Malte Schwerhoff

17

เพื่อชัดเจน HList นั้นไม่มีอะไรมากไปกว่ากองซ้อนที่Tuple2มีน้ำตาลต่างกันเล็กน้อยที่ด้านบน

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

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

ดังนั้นคำถามของคุณเกี่ยวกับความแตกต่างระหว่างการใช้ tuples ซ้อนกับ tuples แบน แต่ทั้งสองเป็น isomorphic ดังนั้นในที่สุดก็ไม่มีความแตกต่างยกเว้นความสะดวกสบายในการใช้งานฟังก์ชั่นของห้องสมุดและสามารถใช้สัญลักษณ์ใดได้บ้าง


สิ่งอันดับสามารถแมปไปยัง hlists และย้อนกลับได้ดังนั้นจึงมีมอร์ฟิซึ่มส์ที่ชัดเจน
Erik Kaplun

10

มีสิ่งต่างๆมากมายที่คุณทำไม่ได้กับสิ่งอันดับ (tuples):

  • เขียนฟังก์ชัน prepend / append ทั่วไป
  • เขียนฟังก์ชั่นย้อนกลับ
  • เขียนฟังก์ชัน concat
  • ...

คุณสามารถทำทั้งหมดนี้ได้ด้วยสิ่งอันดับแน่นอน แต่ไม่ใช่ในกรณีทั่วไป ดังนั้นการใช้ HLists ทำให้โค้ดของคุณแห้งมากขึ้น


8

ฉันสามารถอธิบายสิ่งนี้เป็นภาษาง่าย ๆ :

การตั้งชื่อรายการ tuple vs ไม่มีความหมาย HLists สามารถตั้งชื่อเป็น HTuples ข้อแตกต่างคือใน Scala + Haskell คุณสามารถทำสิ่งนี้ด้วย tuple (โดยใช้ไวยากรณ์ Scala):

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

ในการรับอินพุต tuple ขององค์ประกอบสองชนิดทุกชนิดผนวกองค์ประกอบที่สามและส่งกลับ tuple ที่พิมพ์อย่างสมบูรณ์ด้วยองค์ประกอบสามอย่าง แต่ในขณะที่นี่เป็นประเภททั่วไปอย่างสมบูรณ์มากกว่ามันจะต้องระบุความยาวอินพุต / เอาท์พุตอย่างชัดเจน

ลักษณะของ Haskell คืออะไร HList ให้คุณทำสิ่งนี้แบบทั่วไปมากกว่าความยาวดังนั้นคุณสามารถผนวกความยาวของ tuple / list ใด ๆ และกลับมาที่ tuple / list ที่พิมพ์แบบสแตติก ผลประโยชน์นี้ยังใช้กับคอลเลกชันที่พิมพ์เป็นเนื้อเดียวกันซึ่งคุณสามารถผนวก int ไปยังรายการของ n ints ทั้งหมดและรับกลับรายการที่พิมพ์แบบคงที่เพื่อให้มี ints (n + 1) โดยไม่ต้องระบุ n

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.