อะไรคือกรณีการใช้งานที่น่าสนใจสำหรับประเภทวิธีการที่ต้องพึ่งพา


127

ประเภทวิธีการขึ้นอยู่กับที่เคยเป็นคุณลักษณะทดลองมาก่อนตอนนี้ได้เปิดใช้งานโดยค่าเริ่มต้นในหีบและดูเหมือนว่าสิ่งนี้จะสร้างความตื่นเต้นในชุมชน Scala

ดูครั้งแรกยังไม่ชัดเจนในทันทีว่าสิ่งนี้มีประโยชน์สำหรับอะไร Heiko Seeberger โพสต์ตัวอย่างง่าย ๆ ของประเภทวิธีการที่ขึ้นกับที่นี่ซึ่งสามารถเห็นได้ในความคิดเห็นสามารถทำซ้ำได้อย่างง่ายดายด้วยพารามิเตอร์ประเภทในวิธีการ นั่นไม่ใช่ตัวอย่างที่น่าสนใจ (ฉันอาจจะขาดอะไรบางอย่างที่ชัดเจนโปรดแก้ไขฉันหากมี)

มีตัวอย่างกรณีการใช้งานที่ใช้งานได้จริงและมีประโยชน์อะไรบ้างสำหรับประเภทวิธีการที่พึ่งพาซึ่งพวกเขาได้เปรียบอย่างชัดเจนมากกว่าทางเลือกอื่น ๆ

สิ่งที่น่าสนใจที่เราสามารถทำได้กับสิ่งเหล่านี้ที่ไม่เคยเป็นไปได้ / ง่ายมาก่อน?

พวกเขาซื้ออะไรเราจากคุณสมบัติระบบประเภทที่มีอยู่?

นอกจากนี้ประเภทวิธีการที่ขึ้นอยู่กับการเปรียบเทียบหรือการวาดแรงบันดาลใจจากคุณสมบัติใด ๆ ที่พบในระบบประเภทของภาษาพิมพ์ขั้นสูงอื่น ๆ เช่น Haskell, OCaml หรือไม่


คุณอาจสนใจอ่านhaskell.org/haskellwiki/Dependent_type
Dan Burton

ขอบคุณสำหรับลิงค์ Dan! ฉันทราบถึงประเภทที่ขึ้นต่อกันโดยทั่วไป แต่แนวคิดของประเภทวิธีการพึ่งพานั้นค่อนข้างใหม่สำหรับฉัน
missingfaktor

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

ไม่คุณไม่ได้ทำ แต่เห็นได้ชัดว่าฉันทำ :-) ฉันไม่เห็นความเชื่อมโยงระหว่างทั้งสองมาก่อน ตอนนี้มันใสมาก
missingfaktor

คำตอบ:


112

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

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

ผมจะแสดงให้เห็นว่ามีการออกกำลังกายในระหว่างที่ฉันให้ฉันหลักสูตรการฝึกอบรมขั้นสูง Scala ,

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

เป็นตัวอย่างของรูปแบบเค้กคลาสสิก: เรามีกลุ่มนามธรรมที่ค่อยๆถูกกลั่นกรองผ่านการสืบทอด ( ResourceManager/ ได้ResourceรับการขัดเกลาโดยFileManager/ Fileซึ่งจะถูกขัดเกลาโดยNetworkFileManager/ RemoteFile) เป็นตัวอย่างของเล่น แต่รูปแบบเป็นของจริง: ใช้ตลอดทั้งคอมไพเลอร์ Scala และถูกใช้อย่างกว้างขวางในปลั๊กอิน Scala Eclipse

นี่คือตัวอย่างของสิ่งที่เป็นนามธรรมที่ใช้อยู่

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

โปรดสังเกตว่าการขึ้นต่อกันของพา ธ หมายความว่าคอมไพลเลอร์จะรับประกันว่าtestHashและtestDuplicatesเมธอดบนNetworkFileManagerสามารถเรียกใช้ด้วยอาร์กิวเมนต์ที่สอดคล้องกับมันเท่านั้นเช่น เป็นของตัวเองRemoteFilesและไม่มีอะไรอื่น

นั่นเป็นคุณสมบัติที่ต้องการอย่างปฏิเสธไม่ได้ แต่สมมติว่าเราต้องการย้ายรหัสทดสอบนี้ไปยังไฟล์ต้นฉบับอื่น? ด้วยประเภทเมธอดที่ขึ้นต่อกันมันง่ายมากที่จะกำหนดวิธีการเหล่านั้นใหม่นอกResourceManagerลำดับชั้น

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

สังเกตการใช้ประเภทเมธอดที่เกี่ยวข้องที่นี่: ประเภทของอาร์กิวเมนต์ที่สอง ( rm.Resource) ขึ้นอยู่กับค่าของอาร์กิวเมนต์แรก ( rm)

เป็นไปได้ที่จะทำสิ่งนี้โดยไม่ต้องพึ่งพาประเภทวิธีการ แต่มันก็น่าอึดอัดมากและกลไกก็ไม่ได้ใช้งานง่าย: ฉันสอนหลักสูตรนี้มาเกือบสองปีแล้วและในเวลานั้นไม่มีใครคิดวิธีแก้ปัญหาที่ใช้งานได้

ลองด้วยตัวคุณเอง ...

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

หลังจากต่อสู้กับมันไม่นานคุณอาจจะค้นพบว่าทำไมฉัน (หรืออาจจะเป็น David MacIver เราจำไม่ได้ว่าเราคนไหนเป็นคนบัญญัติศัพท์) เรียกสิ่งนี้ว่า Bakery of Doom

แก้ไข:ฉันทามติคือ Bakery of Doom เป็นเหรียญของ David MacIver ...

สำหรับโบนัส: รูปแบบของประเภทที่ขึ้นต่อกันโดยทั่วไปของ Scala (และประเภทวิธีการขึ้นอยู่กับส่วนหนึ่ง) ได้รับแรงบันดาลใจจากภาษาการเขียนโปรแกรมBeta ... ซึ่งเกิดขึ้นโดยธรรมชาติจากความหมายการซ้อนกันของ Beta ฉันไม่รู้ภาษาการเขียนโปรแกรมกระแสหลักอื่น ๆ ที่มีประเภทขึ้นอยู่กับในรูปแบบนี้ ภาษาเช่น Coq, Cayenne, Epigram และ Agda มีรูปแบบการพิมพ์ที่แตกต่างกันซึ่งมีลักษณะทั่วไปมากกว่า แต่แตกต่างอย่างมีนัยสำคัญโดยเป็นส่วนหนึ่งของระบบประเภทซึ่งไม่เหมือนกับ Scala ที่ไม่มีการพิมพ์ย่อย


2
David MacIver เป็นผู้บัญญัติศัพท์ แต่ไม่ว่าในกรณีใดมันค่อนข้างจะอธิบายได้ นี่เป็นคำอธิบายที่ยอดเยี่ยมว่าเหตุใดประเภทวิธีการที่ต้องพึ่งพาจึงน่าตื่นเต้นมาก ทำได้ดีมาก!
Daniel Spiewak

มันเกิดขึ้นในตอนแรกในการสนทนาระหว่างเราสองคนใน #scala เมื่อไม่นานมานี้ ... เช่นฉันบอกว่าฉันจำไม่ได้ว่าเราคนไหนเป็นคนพูดก่อน
Miles Sabin

ดูเหมือนว่าความทรงจำของฉันกำลังเล่นตลกกับฉัน ... ฉันทามติคือเหรียญของ David MacIver
Miles Sabin

ใช่ฉันไม่ได้อยู่ที่นั่น (ใน #scala) ในเวลานั้น แต่ Jorge อยู่และนั่นคือที่ที่ฉันได้รับข้อมูลของฉัน
Daniel Spiewak

การใช้การปรับแต่งสมาชิกประเภทนามธรรมทำให้ฉันสามารถใช้ฟังก์ชัน testHash4 ได้อย่างไม่ลำบาก def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")ฉันคิดว่าสิ่งนี้ถือได้ว่าเป็นอีกรูปแบบหนึ่งของประเภทที่พึ่งพาได้
Marco van Hilst

53
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

ที่อื่นเราสามารถรับประกันได้ว่าเราจะไม่ผสมโหนดจากกราฟสองกราฟที่แตกต่างกันเช่น:

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

แน่นอนว่าสิ่งนี้ใช้งานได้แล้วหากกำหนดไว้ภายในGraphแต่บอกว่าเราไม่สามารถแก้ไขได้Graphและกำลังเขียนส่วนขยาย "แมงดาห้องสมุดของฉัน" ให้

เกี่ยวกับคำถามที่สอง: ประเภทการใช้งานโดยคุณลักษณะนี้อยู่ห่างไกลที่อ่อนแอกว่าชนิดขึ้นอยู่กับฉบับสมบูรณ์ (ดูdependently พิมพ์การเขียนโปรแกรมใน AGDAสำหรับรสชาติของที่.) ผมไม่คิดว่าผมเคยเห็นการเปรียบเทียบก่อน


6

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

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^

นี่ไม่เกี่ยวกัน ด้วยสมาชิกประเภทคุณสามารถใช้การปรับแต่งเพื่อให้ได้ผลลัพธ์เดียวกัน: trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}ฯลฯ
nafg

ไม่ว่าในกรณีใดสิ่งนี้ไม่เกี่ยวข้องกับประเภทเมธอดที่ขึ้นต่อกัน ยกตัวอย่างเช่นของ Alexey ( stackoverflow.com/a/7860821/333643 ) การใช้แนวทางของคุณ (รวมถึงเวอร์ชันการปรับแต่งที่ฉันแสดงความคิดเห็น) ไม่บรรลุเป้าหมาย เพื่อให้แน่ใจว่า n1.Node =: = n2.Node แต่จะไม่มั่นใจว่าทั้งคู่อยู่ในกราฟเดียวกัน IIUC DMT มั่นใจในสิ่งนี้
nafg

@nafg ขอบคุณที่ชี้ให้เห็น ฉันได้เพิ่มคำว่าคอนกรีตเพื่อให้ชัดเจนว่าฉันไม่ได้หมายถึงกรณีการปรับแต่งสำหรับสมาชิกประเภท เท่าที่ฉันเห็นนี่ยังคงเป็นกรณีการใช้งานที่ถูกต้องสำหรับประเภทเมธอดที่พึ่งพาแม้ว่าประเด็นของคุณ (ซึ่งฉันทราบดี) ว่าสามารถมีอำนาจมากกว่าในกรณีการใช้งานอื่น ๆ หรือฉันพลาดสาระสำคัญพื้นฐานของความคิดเห็นที่สองของคุณ?
Shelby Moore III

3

ฉันกำลังพัฒนาโมเดลสำหรับการทำงานร่วมกันของรูปแบบของการเขียนโปรแกรมเชิงประกาศกับสภาวะแวดล้อม รายละเอียดไม่เกี่ยวข้องที่นี่ (เช่นรายละเอียดเกี่ยวกับการโทรกลับและความคล้ายคลึงกันของแนวคิดกับโมเดลนักแสดงที่รวมกับ Serializer)

ปัญหาที่เกี่ยวข้องคือค่าสถานะจะถูกเก็บไว้ในแผนที่แฮชและอ้างอิงโดยค่าคีย์แฮช ฟังก์ชันป้อนอาร์กิวเมนต์ที่ไม่เปลี่ยนรูปซึ่งเป็นค่าจากสภาพแวดล้อมอาจเรียกใช้ฟังก์ชันดังกล่าวอื่น ๆ และเขียนสถานะไปยังสภาพแวดล้อม แต่ฟังก์ชั่นไม่ได้รับอนุญาตให้อ่านค่าจากสภาพแวดล้อม (ดังนั้นรหัสภายในของฟังก์ชันจึงไม่ขึ้นอยู่กับลำดับของการเปลี่ยนแปลงสถานะและยังคงมีการประกาศในแง่นั้น) จะพิมพ์สิ่งนี้ใน Scala ได้อย่างไร?

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

แต่ถ้าคีย์กัญชาเหล่านี้เป็นสตริงหรือค่าแฮจำนวนเต็มพิมพ์แบบคงที่ของแผนที่ประเภทกัญชาองค์ประกอบsubsumesการใด ๆ หรือ AnyRef (กัญชารหัสแผนที่ไม่ได้แสดงไว้ด้านล่าง) และจึงไม่ตรงกันเวลาทำงานอาจเกิดขึ้นคือมันจะเป็นไปได้ เพื่อใส่ค่าประเภทใดก็ได้ในแผนที่แฮชสำหรับคีย์แฮชที่กำหนด

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

แม้ว่าฉันจะไม่ได้ทดสอบสิ่งต่อไปนี้ แต่ในทางทฤษฎีฉันสามารถรับคีย์แฮชจากชื่อคลาสที่รันไทม์ได้classOfดังนั้นแฮชคีย์จึงเป็นชื่อคลาสแทนสตริง (โดยใช้แบ็กติกของสกาล่าเพื่อฝังสตริงในชื่อคลาส)

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

จึงได้ความปลอดภัยแบบคงที่

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A

เมื่อเราต้องการที่จะผ่านปุ่มอาร์กิวเมนต์รอบในค่าเดียวผมไม่ได้ทดสอบ แต่ถือว่าเราสามารถใช้ Tuple def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): Aเช่นการเกิน เราจะไม่ใช้ชุดของคีย์อาร์กิวเมนต์เนื่องจากประเภทขององค์ประกอบจะถูกย่อย (ไม่ทราบที่เวลาคอมไพล์) ในประเภทของคอลเล็กชัน
Shelby Moore III

"การพิมพ์แบบคงที่ขององค์ประกอบแผนที่แฮชจะเปลี่ยนเป็น Any หรือ AnyRef" - ฉันไม่ทำตาม เมื่อคุณพูดว่าประเภทองค์ประกอบคุณหมายถึงประเภทคีย์หรือประเภทค่า (เช่นอาร์กิวเมนต์ประเภทที่หนึ่งหรือที่สองสำหรับ HashMap) แล้วทำไมมันถึงถูกย่อย?
Robin Green

@RobinGreen ประเภทของค่าในตารางแฮช Afair ถูกย่อยเนื่องจากคุณไม่สามารถใส่มากกว่าหนึ่งประเภทในคอลเล็กชันใน Scala เว้นแต่คุณจะสมัครเป็น supertype ทั่วไปเนื่องจาก Scala ไม่มีประเภทการรวมกัน (disjunction) ดูคำถาม & คำตอบของฉันเกี่ยวกับการย่อยใน Scala
Shelby Moore III
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.