พิมพ์คลาส vs วัตถุอินเตอร์เฟส


33

ฉันไม่คิดว่าฉันเข้าใจคลาสประเภท ฉันได้อ่านที่ไหนสักแห่งที่คิดว่า class classes เป็น "interfaces" (จาก OO) ที่การใช้งานประเภทนั้นผิดและทำให้เข้าใจผิด ปัญหาคือฉันมีปัญหาที่เห็นพวกเขาเป็นสิ่งที่แตกต่างและวิธีการที่ผิด

ตัวอย่างเช่นถ้าฉันมีคลาสประเภท (ในไวยากรณ์ Haskell)

class Functor f where
  fmap :: (a -> b) -> f a -> f b

มันแตกต่างจากอินเตอร์เฟส [1] อย่างไร (ในไวยากรณ์ Java)

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

ความแตกต่างที่เป็นไปได้อย่างหนึ่งที่ฉันคิดได้ก็คือการใช้งานคลาสประเภทที่ใช้ในการเรียกใช้บางอย่างไม่ได้ระบุ แต่พิจารณาจากสภาพแวดล้อม - กล่าวโดยตรวจสอบโมดูลที่มีอยู่สำหรับการใช้งานสำหรับประเภทนี้ ดูเหมือนว่าจะเป็นสิ่งประดิษฐ์ในการนำไปปฏิบัติที่อาจกล่าวถึงเป็นภาษา OO เช่นคอมไพเลอร์ (หรือรันไทม์) สามารถสแกนหา wrapper / extender / monkey-patcher ที่แสดงอินเตอร์เฟสที่จำเป็นในประเภท

ฉันพลาดอะไรไป

[1] โปรดสังเกตว่าการf aโต้แย้งได้ถูกลบออกไปแล้วfmapเนื่องจากเป็นภาษา OO คุณจะต้องเรียกวิธีนี้บนวัตถุ อินเทอร์เฟซนี้จะถือว่าf aอาร์กิวเมนต์ได้รับการแก้ไขแล้ว

คำตอบ:


46

ในรูปแบบพื้นฐานคลาสประเภทค่อนข้างคล้ายกับอินเตอร์เฟสวัตถุ อย่างไรก็ตามในหลาย ๆ ด้านพวกเขาเป็นคนทั่วไปมากกว่า

  1. การจัดส่งอยู่ในประเภทไม่ใช่ค่า ไม่จำเป็นต้องมีค่าในการดำเนินการ ตัวอย่างเช่นมันเป็นไปได้ที่จะทำการจัดส่งในประเภทผลลัพธ์ของฟังก์ชันเช่นเดียวกับReadคลาสของ Haskell :

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    การจัดส่งดังกล่าวเป็นไปไม่ได้อย่างชัดเจนใน OO ทั่วไป

  2. คลาสของประเภทจะขยายไปถึงการแจกจ่ายหลายอย่างโดยง่ายโดยการให้พารามิเตอร์หลายตัว:

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. คำนิยามอินสแตนซ์เป็นอิสระจากคำจำกัดความของคลาสและประเภทซึ่งทำให้เป็นโมดุลมากขึ้น ประเภท T จากโมดูล A สามารถดัดแปลงเป็นคลาส C จากโมดูล M2 โดยไม่ต้องแก้ไขคำจำกัดความของทั้งสองเพียงแค่ให้ตัวอย่างในโมดูล M3 ใน OO สิ่งนี้ต้องการคุณสมบัติทางภาษาที่ลึกลับ (และ OO-ish) ที่น้อยลงเช่นวิธีการขยาย

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

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    เมื่อเทียบกับ

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    ในกรณีก่อนหน้าการใช้pick '\0' 'x'มีประเภทCharในขณะที่ในกรณีหลังสิ่งที่คุณรู้เกี่ยวกับผลลัพธ์จะเป็น Enum (นี่เป็นเหตุผลว่าทำไมภาษา OO ส่วนใหญ่ในทุกวันนี้จึงรวมตัวแปรหลายตัวแปรเข้าด้วยกัน)

  5. ที่เกี่ยวข้องอย่างใกล้ชิดเป็นปัญหาของวิธีการแบบไบนารี พวกมันเป็นธรรมชาติอย่างสมบูรณ์พร้อมคลาสประเภท:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    ด้วยการพิมพ์ย่อยเพียงอย่างเดียวOrdอินเตอร์เฟสจะไม่สามารถแสดงออกได้ คุณต้องการรูปแบบการเรียกซ้ำที่ซับซ้อนหรือแบบพาราเมตริกที่เรียกว่า "การหาปริมาณ F-bounded" เพื่อให้มันถูกต้อง เปรียบเทียบ Java Comparableและการใช้งาน:

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

ในทางตรงกันข้ามอินเทอร์เฟซที่ใช้ชนิดย่อยที่อนุญาตให้สร้างคอลเลกชันที่แตกต่างกันตามธรรมชาติเช่นรายการประเภทList<C>สามารถประกอบด้วยสมาชิกที่มีชนิดย่อยต่าง ๆ ของC(แม้ว่ามันเป็นไปไม่ได้ที่จะกู้คืนประเภทที่แน่นอนยกเว้นโดยใช้ downcasts) ในการทำเช่นเดียวกันกับคลาสประเภทคุณต้องมีประเภทที่มีอยู่เป็นคุณลักษณะเพิ่มเติม


อานั่นทำให้รู้สึกมาก การจ่ายตามประเภทเทียบกับมูลค่าน่าจะเป็นเรื่องใหญ่ที่ฉันไม่ได้คิดอย่างถูกต้อง ปัญหาของตัวแปรหลากหลายและการพิมพ์ที่เฉพาะเจาะจงมากขึ้นทำให้รู้สึก ฉันเพิ่งดึงสิ่งนั้นและอินเทอร์เฟซที่ใช้ประเภทย่อยเข้าด้วยกันในใจของฉัน (เห็นได้ชัดว่าฉันคิดใน Java: - /)
oconnor0

อัตถิภาวนิยมเป็นสิ่งที่คล้ายกับการสร้างชนิดย่อยCโดยไม่มีการปรากฏตัวของ downcasts หรือไม่?
oconnor0

ชนิดของ พวกเขาเป็นเครื่องมือสำหรับการสร้างบทคัดย่อประเภทเช่นการซ่อนการเป็นตัวแทน ใน Haskell ถ้าคุณแนบข้อ จำกัด ของคลาสไว้ด้วยคุณยังสามารถใช้วิธีการของคลาสเหล่านั้นได้ - Downcasts เป็นคุณลักษณะที่แยกจากทั้งการพิมพ์ย่อยและการหาปริมาณของอัตถิภาวนิยมและโดยหลักการแล้วสามารถเพิ่มในการปรากฏตัวของสิ่งหลังได้เช่นกัน เช่นเดียวกับภาษา OO ที่ไม่ได้ให้บริการ
Andreas Rossberg

PS: FWIW, wildcard types ใน Java เป็นประเภทที่มีอยู่แม้ว่าจะค่อนข้าง จำกัด และ ad-hoc (ซึ่งอาจเป็นส่วนหนึ่งของสาเหตุที่ทำให้พวกเขาสับสน)
Andreas Rossberg

1
@didierc ที่จะถูก จำกัด ในกรณีที่สามารถแก้ไขได้แบบสแตติก ยิ่งไปกว่านั้นในการจับคู่คลาสประเภทนั้นจะต้องใช้รูปแบบของการแก้ปัญหาการโอเวอร์โหลดที่สามารถแยกความแตกต่างตามประเภทการส่งคืนเพียงอย่างเดียว (ดูรายการ 1)
Andreas Rossberg

6

นอกจากคำตอบที่ยอดเยี่ยมของ Andreas โปรดจำไว้ว่าคลาสประเภทนั้นมีไว้เพื่อปรับปรุงการโอเวอร์โหลดซึ่งส่งผลต่อพื้นที่ชื่อโกลบอล ไม่มีการบรรทุกเกินพิกัดใน Haskell นอกเหนือจากสิ่งที่คุณสามารถได้รับผ่านคลาสประเภท ในทางตรงกันข้ามเมื่อคุณใช้อินเตอร์เฟสวัตถุเฉพาะฟังก์ชั่นที่ได้รับการประกาศให้รับอาร์กิวเมนต์ของอินเตอร์เฟสนั้นจะต้องกังวลเกี่ยวกับชื่อฟังก์ชั่นในอินเตอร์เฟสนั้น ดังนั้นอินเตอร์เฟสจึงจัดเตรียมช่องว่างชื่อโลคัล

ตัวอย่างเช่นคุณมีfmapอินเทอร์เฟซวัตถุชื่อ "Functor" มันคงจะโอเคที่จะมีอีกอันfmapในอินเทอร์เฟซอื่นพูดว่า "ตัวสร้าง" แต่ละอ็อบเจ็กต์ (หรือคลาส) สามารถเลือกและเลือกอินเตอร์เฟสที่ต้องการนำไปใช้ ในทางตรงกันข้ามใน Haskell คุณสามารถมีเพียงหนึ่งเดียวfmapในบริบทที่เฉพาะเจาะจง คุณไม่สามารถนำเข้าทั้งประเภท Functor และ Structor ลงในบริบทเดียวกัน

ส่วนต่อประสานวัตถุนั้นคล้ายคลึงกับลายเซ็นมาตรฐาน ML มากกว่าการพิมพ์คลาส


และดูเหมือนว่าจะมีความสัมพันธ์ใกล้ชิดระหว่างโมดูล ML และคลาสประเภท Haskell cse.unsw.edu.au/~chak/papers/DHC07.html
Steven Shaw

1

ในตัวอย่างที่เป็นรูปธรรมของคุณ (ด้วยคลาสประเภท Functor) การใช้งาน Haskell และ Java จะทำงานแตกต่างกัน ลองนึกภาพว่าคุณมีประเภทข้อมูลบางทีและต้องการให้เป็น Functor (เป็นประเภทข้อมูลที่นิยมมากใน Haskell ซึ่งคุณสามารถนำไปใช้ใน Java ได้อย่างง่ายดายเช่นกัน) ในตัวอย่าง Java ของคุณคุณจะทำให้บางทีคลาสใช้อินเทอร์เฟซ Functor ของคุณ ดังนั้นคุณสามารถเขียนต่อไปนี้ (เพียงแค่หลอกรหัสเพราะฉันมีพื้นหลัง c # เท่านั้น):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

ขอให้สังเกตว่าresมีประเภท Functor ไม่อาจ ดังนั้นสิ่งนี้ทำให้การติดตั้ง Java ไม่สามารถใช้งานได้เนื่องจากคุณสูญเสียข้อมูลประเภทที่เป็นรูปธรรมและต้องปลดเปลื้อง (อย่างน้อยฉันก็ล้มเหลวในการเขียนการใช้งานที่ยังคงมีประเภท) ด้วยคลาสประเภท Haskell คุณจะได้รับบางที Int เป็นผลลัพธ์


ฉันคิดว่าปัญหานี้เกิดจาก Java ไม่สนับสนุนประเภทที่สูงกว่าและไม่เกี่ยวข้องกับการอภิปราย Vs typeclasses ของอินเทอร์เฟซ ถ้า Java มีชนิดที่สูงกว่า fmap ก็สามารถคืนค่าได้เป็นMaybe<Int>อย่างดี
dcastro
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.