ประเภทที่สูงกว่ามีประโยชน์เมื่อใด


89

ฉันทำ dev ใน F # มาระยะหนึ่งแล้วและฉันก็ชอบมัน อย่างไรก็ตามคำศัพท์หนึ่งที่ฉันรู้ว่าไม่มีอยู่ใน F # เป็นประเภทที่สูงกว่า ฉันได้อ่านเนื้อหาเกี่ยวกับประเภทที่สูงกว่าและฉันคิดว่าฉันเข้าใจคำจำกัดความของพวกเขา ฉันไม่แน่ใจว่าเหตุใดจึงมีประโยชน์ ใครช่วยยกตัวอย่างว่าประเภทที่สูงกว่าทำให้ง่ายใน Scala หรือ Haskell ที่ต้องใช้วิธีแก้ปัญหาใน F # สำหรับตัวอย่างเหล่านี้วิธีแก้ปัญหาจะเป็นอย่างไรหากไม่มีประเภทสูงกว่า (หรือในทางกลับกันใน F #) บางทีฉันอาจจะชินกับการทำงานกับมันมากจนฉันไม่สังเกตเห็นว่าไม่มีฟีเจอร์นั้น

(ฉันคิดว่า) ฉันเข้าใจว่าแทนที่จะเป็นประเภทที่ดีกว่าmyList |> List.map fหรือmyList |> Seq.map f |> Seq.toListสูงกว่าช่วยให้คุณเขียนได้ง่ายๆmyList |> map fและมันจะส่งกลับ a List. มันเยี่ยมมาก (สมมติว่าถูกต้อง) แต่ดูเหมือนจะไม่สุภาพ? (และไม่สามารถทำได้ง่ายๆโดยปล่อยให้ฟังก์ชันโอเวอร์โหลด) ฉันมักจะแปลงเป็นSeqอย่างไรก็ตามจากนั้นฉันสามารถแปลงเป็นอะไรก็ได้ที่ฉันต้องการในภายหลัง อีกครั้งบางทีฉันก็ชินเกินไปที่จะทำงานกับมัน แต่จะมีตัวอย่างที่ใดประเภทสูง kinded จริงๆช่วยให้คุณประหยัดทั้งในการกดแป้นพิมพ์หรือในรูปแบบความปลอดภัย?


2
ฟังก์ชั่นมากมายในการควบคุมMonadใช้ประโยชน์จากชนิดที่สูงกว่าดังนั้นคุณอาจต้องการดูตัวอย่าง ใน F # การใช้งานจะต้องทำซ้ำสำหรับ monad คอนกรีตแต่ละประเภท
ลี

1
@ ลี แต่คุณไม่สามารถสร้างอินเทอร์เฟซIMonad<T>แล้วส่งกลับไปที่เช่นIEnumerable<int>หรือIObservable<int>เมื่อคุณทำเสร็จแล้ว? ทั้งหมดนี้เป็นเพียงเพื่อหลีกเลี่ยงการแคสต์หรือไม่?
กุ้งมังกร

4
การหล่ออย่างดีนั้นไม่ปลอดภัยดังนั้นจึงตอบคำถามของคุณเกี่ยวกับความปลอดภัยของประเภท ปัญหาอีกประการหนึ่งคือreturnจะทำงานอย่างไรเนื่องจากเป็นของประเภท monad ไม่ใช่อินสแตนซ์เฉพาะดังนั้นคุณจึงไม่ต้องการวางไว้ในIMonadอินเทอร์เฟซเลย
ลี

4
@ ลีใช่ฉันแค่คิดว่าคุณจะต้องส่งผลลัพธ์สุดท้ายหลังจากการแสดงออกไม่ใช่เรื่องใหญ่เพราะคุณเพิ่งสร้างนิพจน์เพื่อให้คุณรู้ประเภท แต่ดูเหมือนว่าคุณจะต้องร่ายในแต่ละนัยของbindอาคาSelectManyฯลฯ ด้วย ซึ่งหมายความว่าใครบางคนสามารถใช้ API เพื่อbindan IObservableถึงIEnumerableและคิดว่ามันจะใช้งานได้ซึ่งใช่ถ้าเป็นเช่นนั้นและไม่มีทางแก้ไข แค่ไม่แน่ใจ 100% ว่าไม่มีทางรอบข้าง
กุ้งมังกร

6
คำถามที่ดี ฉันยังไม่เห็นตัวอย่างการใช้งานจริงที่น่าสนใจเพียงตัวอย่างเดียวของคุณลักษณะภาษานี้ที่มีประโยชน์ IRL
JD

คำตอบ:


79

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

data List a = Cons a (List a) | Nil

ตัวสร้างListชนิดมีชนิด* -> *ซึ่งหมายความว่าจะต้องผ่านประเภทคอนกรีตเพื่อให้ได้ประเภทคอนกรีต: List Intสามารถมีผู้อยู่อาศัยเหมือน[1,2,3]แต่Listตัวเองทำไม่ได้

ฉันจะสมมติว่าประโยชน์ของคอนเทนเนอร์โพลีมอร์ฟิกนั้นชัดเจน แต่มีประเภทที่มีประโยชน์* -> *มากกว่าแค่คอนเทนเนอร์ ตัวอย่างเช่นความสัมพันธ์

data Rel a = Rel (a -> a -> Bool)

หรือตัวแยกวิเคราะห์

data Parser a = Parser (String -> [(a, String)])

* -> *ทั้งยังมีชนิด


อย่างไรก็ตามเราสามารถใช้สิ่งนี้ต่อไปใน Haskell โดยการมีประเภทที่มีลำดับสูงกว่า (* -> *) -> *ยกตัวอย่างเช่นเราสามารถมองหาชนิดกับชนิด ตัวอย่างง่ายๆของสิ่งนี้อาจเป็นการShapeพยายามเติมภาชนะชนิด* -> *หนึ่ง

data Shape f = Shape (f ())

[(), (), ()] :: Shape List

สิ่งนี้มีประโยชน์สำหรับการระบุลักษณะTraversables ใน Haskell เนื่องจากสามารถแบ่งออกเป็นรูปร่างและเนื้อหาได้เสมอ

split :: Traversable t => t a -> (Shape t, [a])

อีกตัวอย่างหนึ่งลองพิจารณาต้นไม้ที่กำหนดพารามิเตอร์ตามชนิดของกิ่งก้านนั้น ตัวอย่างเช่นต้นไม้ธรรมดาอาจเป็น

data Tree a = Branch (Tree a) a (Tree a) | Leaf

แต่เราจะเห็นว่าประเภทของกิ่งประกอบด้วย a PairของTree as ดังนั้นเราจึงสามารถแยกชิ้นส่วนนั้นออกจากประเภทพาราเมตริกได้

data TreeG f a = Branch a (f (TreeG f a)) | Leaf

data Pair a = Pair a a
type Tree a = TreeG Pair a

นี้คอนสตรัคชนิดมีชนิดTreeG (* -> *) -> * -> *เราสามารถใช้เพื่อสร้างรูปแบบอื่น ๆ ที่น่าสนใจเช่นไฟล์RoseTree

type RoseTree a = TreeG [] a

rose :: RoseTree Int
rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]

หรือพยาธิสภาพเช่นก MaybeTree

data Empty a = Empty
type MaybeTree a = TreeG Empty a

nothing :: MaybeTree a
nothing = Leaf

just :: a -> MaybeTree a
just a = Branch a Empty

หรือก TreeTree

type TreeTree a = TreeG Tree a

treetree :: TreeTree Int
treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))

สถานที่อื่นที่ปรากฏคือใน "algebras of functors" sum :: [Int] -> Intถ้าเราลดลงไม่กี่ชั้นของนามธรรมนี้อาจได้รับการพิจารณาดีขึ้นเป็นเท่าเช่น algebras จะแปรมากกว่าfunctorและผู้ให้บริการ functorมีชนิด* -> *และผู้ให้บริการชนิด*อื่น ๆ โดยสิ้นเชิง

data Alg f a = Alg (f a -> a)

(* -> *) -> * -> *มีชนิด Algมีประโยชน์เนื่องจากความสัมพันธ์กับประเภทข้อมูลและรูปแบบการเรียกซ้ำที่สร้างขึ้นบนยอดเหล่านั้น

-- | The "single-layer of an expression" functor has kind `(* -> *)`
data ExpF x = Lit Int
            | Add x x
            | Sub x x
            | Mult x x

-- | The fixed point of a functor has kind `(* -> *) -> *`
data Fix f = Fix (f (Fix f))

type Exp = Fix ExpF

exp :: Exp
exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4

fold :: Functor f => Alg f a -> Fix f -> a
fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)

สุดท้ายแม้ว่าพวกเขาจะเป็นไปได้ในทางทฤษฎีผมไม่เคยเห็นแม้แต่ตัวสร้างประเภทสูง kinded บางครั้งเราจะเห็นฟังก์ชันประเภทนั้น ๆ เช่นmask :: ((forall a. IO a -> IO a) -> IO b) -> IO bแต่ฉันคิดว่าคุณจะต้องเจาะลึกลงไปในประเภท prolog หรือพิมพ์วรรณกรรมที่ขึ้นอยู่กับระดับเพื่อดูระดับความซับซ้อนในประเภทนั้น ๆ


3
ฉันจะพิมพ์ตรวจสอบและแก้ไขโค้ดในไม่กี่นาทีตอนนี้ฉันกำลังใช้โทรศัพท์
J. Abrahamson

12
@ J. Abrahamson +1 สำหรับคำตอบที่ดีและมีความอดทนในการพิมพ์บนโทรศัพท์ของคุณ O_o
Daniel Gratzer

3
@lobsterism A TreeTreeเป็นเพียงพยาธิสภาพ แต่ในทางปฏิบัติยิ่งกว่านั้นหมายความว่าคุณมีต้นไม้สองประเภทที่เชื่อมโยงกัน - การผลักดันแนวคิดนั้นให้ไกลออกไปอีกเล็กน้อยจะทำให้คุณได้รับแนวคิดที่ปลอดภัยที่มีประสิทธิภาพเช่นสีแดง / ต้นไม้สีดำและประเภท FingerTree ที่สมดุลแบบคงที่
J. Abrahamson

3
@JonHarrop ตัวอย่างมาตรฐานในโลกแห่งความเป็นจริงเป็นนามธรรมมากกว่า monads เช่นมีเอฟเฟกต์แบบ mtl stacks คุณอาจไม่เห็นด้วยว่านี่เป็นสิ่งที่มีค่าในโลกแห่งความเป็นจริง ฉันคิดว่าโดยทั่วไปเป็นที่ชัดเจนว่าภาษาสามารถดำรงอยู่ได้โดยไม่ต้องมี HKT ดังนั้นตัวอย่างใด ๆ ที่จะนำเสนอสิ่งที่เป็นนามธรรมซึ่งมีความซับซ้อนมากกว่าภาษาอื่น ๆ
J. Abrahamson

2
คุณสามารถมีเช่นชุดย่อยของเอฟเฟกต์ที่ได้รับอนุญาตใน monads ต่างๆและบทคัดย่อเหนือ monads ที่ตรงตามข้อกำหนดนั้น ตัวอย่างเช่น monads สร้างอินสแตนซ์ "teletype" ซึ่งเปิดใช้งานการอ่านและเขียนระดับตัวอักษรอาจรวมทั้ง IO และนามธรรมไปป์ คุณสามารถสรุปเกี่ยวกับการใช้งานแบบอะซิงโครนัสต่างๆเป็นอีกตัวอย่างหนึ่ง หากไม่มี HKT คุณจะ จำกัด ประเภทใด ๆ ที่ประกอบด้วยชิ้นส่วนทั่วไปนั้น
J. Abrahamson

64

พิจารณาFunctorประเภทคลาสใน Haskell ซึ่งfเป็นตัวแปรประเภทที่สูงกว่า:

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

สิ่งที่ลายเซ็นประเภทนี้กล่าวคือ fmap จะเปลี่ยนพารามิเตอร์ type ของfจากaถึงbเป็น แต่ปล่อยfให้เหมือนเดิม ดังนั้นหากคุณใช้fmapกับรายการคุณจะได้รับรายชื่อหากคุณใช้กับโปรแกรมแยกวิเคราะห์คุณจะได้โปรแกรมแยกวิเคราะห์และอื่น ๆ และนี่คือการรับประกันแบบคงที่และเวลาคอมไพล์

ฉันไม่รู้ F # แต่ลองพิจารณาว่าจะเกิดอะไรขึ้นถ้าเราพยายามแสดงสิ่งที่Functorเป็นนามธรรมในภาษาเช่น Java หรือ C # ด้วยการถ่ายทอดทางพันธุกรรมและชื่อสามัญ แต่ไม่มีชื่อสามัญที่สูงกว่า ครั้งแรกลอง:

interface Functor<A> {
    Functor<B> map(Function<A, B> f);
}

ปัญหานี้ลองครั้งแรกก็คือว่าการดำเนินการของอินเตอร์เฟซที่ได้รับอนุญาตให้กลับใด ๆFunctorคลาสที่ใช้ ใครบางคนสามารถเขียนวิธีFunnyList<A> implements Functor<A>ที่มีmapวิธีการส่งคืนคอลเลกชันประเภทอื่นหรือแม้แต่อย่างอื่นที่ไม่ใช่คอลเล็กชันเลย แต่ยังคงเป็นFunctorไฟล์. นอกจากนี้เมื่อคุณใช้mapวิธีนี้คุณจะไม่สามารถเรียกใช้เมธอดเฉพาะประเภทย่อยใด ๆ ในผลลัพธ์ได้เว้นแต่คุณจะดาวน์แคสต์เป็นประเภทที่คุณต้องการจริงๆ ดังนั้นเราจึงมีปัญหาสองประการ:

  1. ระบบประเภทไม่อนุญาตให้เราแสดงความไม่แปรเปลี่ยนที่mapเมธอดส่งคืนFunctorคลาสย่อยเดียวกันกับตัวรับเสมอ
  2. ดังนั้นจึงไม่มีลักษณะที่ปลอดภัยแบบคงที่ในการเรียกใช้Functorวิธีที่ไม่ใช่วิธีการที่เป็นผลมาจากmap.

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

interface Collection<A> extends Functor<A> {
    Collection<B> map(Function<A, B> f);
}

interface List<A> extends Collection<A> {
    List<B> map(Function<A, B> f);
}

interface Set<A> extends Collection<A> {
    Set<B> map(Function<A, B> f);
}

interface Parser<A> extends Functor<A> {
    Parser<B> map(Function<A, B> f);
}

// …

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

( แก้ไข:และโปรดทราบว่าสิ่งนี้ใช้งานได้เพราะFunctor<B>ปรากฏเป็นประเภทผลลัพธ์เท่านั้นดังนั้นอินเทอร์เฟซลูกจึงสามารถ จำกัด ให้แคบลงได้ดังนั้น AFAIK เราจึงไม่สามารถ จำกัด การใช้ทั้งสองอย่างให้แคบลงMonad<B>ในอินเทอร์เฟซต่อไปนี้:

interface Monad<A> {
    <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

ใน Haskell ที่มีตัวแปรประเภทที่มีอันดับสูงกว่านี่คือ(>>=) :: Monad m => m a -> (a -> m b) -> m b)

อีกวิธีหนึ่งคือการใช้ชื่อเรียกซ้ำเพื่อลองและให้อินเทอร์เฟซ จำกัด ประเภทผลลัพธ์ของประเภทย่อยไว้ที่ประเภทย่อยเอง ตัวอย่างของเล่น:

/**
 * A semigroup is a type with a binary associative operation.  Law:
 *
 * > x.append(y).append(z) = x.append(y.append(z))
 */
interface Semigroup<T extends Semigroup<T>> {
    T append(T arg);
}

class Foo implements Semigroup<Foo> {
    // Since this implements Semigroup<Foo>, now this method must accept 
    // a Foo argument and return a Foo result. 
    Foo append(Foo arg);
}

class Bar implements Semigroup<Bar> {
    // Any of these is a compilation error:

    Semigroup<Bar> append(Semigroup<Bar> arg);

    Semigroup<Foo> append(Bar arg);

    Semigroup append(Bar arg);

    Foo append(Bar arg);

}

แต่เทคนิคประเภทนี้ (ซึ่งค่อนข้างจะเป็นความลับสำหรับนักพัฒนา OOP ที่ดำเนินการโดยโรงงานของคุณ แต่สำหรับนักพัฒนาที่ทำงานได้โดยใช้โรงงานของคุณเช่นกัน) ยังไม่สามารถแสดงFunctorข้อ จำกัดที่ต้องการได้:

interface Functor<FA extends Functor<FA, A>, A> {
    <FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}

นี่คือปัญหานี้ไม่ได้ จำกัดFBที่จะมีเหมือนกันFเป็นFA-So ว่าเมื่อคุณประกาศประเภทList<A> implements Functor<List<A>, A>ที่mapวิธีการที่สามารถยังคงNotAList<B> implements Functor<NotAList<B>, B>กลับมาเป็น

ลองขั้นสุดท้ายใน Java โดยใช้ประเภทดิบ (คอนเทนเนอร์ที่ไม่ได้พารามิเตอร์):

interface FunctorStrategy<F> {
    F map(Function f, F arg);
} 

ที่นี่Fจะได้รับ instantiated ประเภท unparametrized เช่นเดียวหรือList Mapสิ่งนี้รับประกันได้ว่า a FunctorStrategy<List>สามารถคืนค่า a Listได้เท่านั้นแต่คุณได้ละทิ้งการใช้ตัวแปรประเภทเพื่อติดตามประเภทองค์ประกอบของรายการ

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

interface Functor<F, A> {
    <B> F<B> map(Function<A, B> f);
}

class List<A> implements Functor<List, A> {

    // Since F := List, F<B> := List<B>
    <B> List<B> map(Function<A, B> f) {
        // ...
    }

}

และกล่าวถึงบิตนี้โดยเฉพาะ:

(ฉันคิดว่า) ฉันเข้าใจว่าแทนที่จะเป็นประเภทที่ดีกว่าmyList |> List.map fหรือmyList |> Seq.map f |> Seq.toListสูงกว่าช่วยให้คุณเขียนได้ง่ายๆmyList |> map fและมันจะส่งกลับ a List. มันเยี่ยมมาก (สมมติว่าถูกต้อง) แต่ดูเหมือนจะไม่สุภาพ? (และไม่สามารถทำได้ง่ายๆโดยปล่อยให้ฟังก์ชันโอเวอร์โหลด) ฉันมักจะแปลงเป็นSeqอย่างไรก็ตามจากนั้นฉันสามารถแปลงเป็นอะไรก็ได้ที่ฉันต้องการในภายหลัง

มีหลายภาษาที่สรุปแนวคิดของmapฟังก์ชันด้วยวิธีนี้โดยการสร้างแบบจำลองราวกับว่าหัวใจสำคัญคือการทำแผนที่เกี่ยวกับลำดับ คำพูดของเธอนี้อยู่ในจิตวิญญาณว่าถ้าคุณมีประเภทที่แปลงสนับสนุนไปและกลับจากSeqคุณจะได้รับการดำเนินการแผนที่ "ฟรี" โดย Seq.mapreusing

อย่างไรก็ตามใน Haskell Functorชั้นเรียนทั่วไปมากกว่านั้น มันไม่ได้เชื่อมโยงกับแนวคิดของลำดับ คุณสามารถนำไปใช้fmapกับประเภทที่ไม่มีการแมปกับลำดับที่ดีเช่นIOการดำเนินการตัวรวมตัวแยกวิเคราะห์ฟังก์ชัน ฯลฯ :

instance Functor IO where
    fmap f action =
        do x <- action
           return (f x)

 -- This declaration is just to make things easier to read for non-Haskellers 
newtype Function a b = Function (a -> b)

instance Functor (Function a) where
    fmap f (Function g) = Function (f . g)  -- `.` is function composition

แนวคิดของ "การทำแผนที่" ไม่ได้เชื่อมโยงกับลำดับ ควรทำความเข้าใจกับกฎหมาย functor:

(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs

อย่างไม่เป็นทางการ:

  1. กฎข้อแรกกล่าวว่าการทำแผนที่ด้วยฟังก์ชัน identity / noop เหมือนกับการไม่ทำอะไรเลย
  2. กฎข้อที่สองกล่าวว่าผลลัพธ์ใด ๆ ที่คุณสามารถสร้างได้โดยการทำแผนที่สองครั้งคุณสามารถสร้างได้โดยการทำแผนที่เพียงครั้งเดียว

นี่คือเหตุผลที่คุณต้องการfmapรักษาประเภทไว้เพราะทันทีที่คุณได้รับmapการดำเนินการที่ให้ผลลัพธ์ประเภทอื่นการรับประกันแบบนี้จะยากขึ้นมาก


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

1
อ่าเข้าใจแล้ว: คุณสามารถสร้าง fn doubleของ functor โดยที่double [1, 2, 3]ให้[2, 4, 6]และdouble sinให้ fn ที่บาปเป็นสองเท่า ฉันสามารถดูได้ว่าถ้าคุณเริ่มคิดในกรอบความคิดนั้นเมื่อคุณเรียกใช้แผนที่บนอาร์เรย์คุณจะคาดหวังว่าอาร์เรย์จะกลับมาไม่ใช่แค่ seq เพราะเรากำลังทำงานกับอาร์เรย์ที่นี่
กุ้งมังกร

@lobsterism: มีอัลกอริทึม / เทคนิคที่ต้องอาศัยความสามารถในการแยกแยะ a Functorและปล่อยให้ไคลเอนต์ของห้องสมุดเลือกมันออกมา คำตอบของ J. Abrahamson ให้ตัวอย่างหนึ่ง: การพับซ้ำสามารถทำให้เป็นเรื่องทั่วไปได้โดยใช้ functors อีกตัวอย่างหนึ่งคือ monads ฟรี คุณสามารถคิดเหล่านี้เป็นชนิดของการดำเนินงานห้องสมุดล่ามทั่วไปที่ลูกค้าวัสดุ "ชุดคำสั่ง" Functorในฐานะที่เป็นพล
Luis Casillas

3
คำตอบที่ดีในทางเทคนิค แต่ทำให้ฉันสงสัยว่าทำไมทุกคนถึงต้องการสิ่งนี้ในทางปฏิบัติ ฉันไม่พบว่าตัวเองไปถึง Haskell's Functorหรือ a SemiGroup. โปรแกรมจริงส่วนใหญ่ใช้คุณลักษณะภาษานี้ที่ไหน
JD

28

ฉันไม่ต้องการให้ข้อมูลซ้ำกับคำตอบที่ยอดเยี่ยมที่นี่ แต่มีประเด็นสำคัญที่ฉันต้องการเพิ่ม

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

โดยทั่วไปผมได้พบว่าเมื่อมีคนไม่เห็นประโยชน์ของ functors / monads / whatevers ก็มักจะเป็นเพราะพวกเขากำลังความคิดของสิ่งเหล่านี้ในช่วงเวลาหนึ่ง การดำเนินการ Functor / monad / etc ไม่เพิ่มอะไรให้กับอินสแตนซ์ใด ๆ (แทนที่จะเรียกการผูก, fmap, ฯลฯ ฉันสามารถเรียกการดำเนินการอะไรก็ได้ที่ฉันใช้ในการใช้การผูก fmap ฯลฯ ) สิ่งที่คุณต้องการจริงๆนามธรรมเหล่านี้เพื่อให้คุณสามารถมีรหัสที่ทำงานโดยทั่วไปกับใด ๆ functor / monad / ฯลฯ

ในบริบทที่มีการใช้รหัสทั่วไปเช่นนี้หมายความว่าเมื่อใดก็ตามที่คุณเขียนอินสแตนซ์ monad ใหม่ประเภทของคุณจะสามารถเข้าถึงการดำเนินการที่มีประโยชน์จำนวนมากที่ได้เขียนไว้ให้คุณได้ทันที นั่นคือจุดที่เห็น monads (และ functors และ ... ) ทุกที่ ไม่ใช่เพื่อให้ฉันสามารถใช้bindแทนที่จะใช้concatและmapนำไปใช้myFunkyListOperation(ซึ่งไม่ทำให้ฉันไม่มีอะไรในตัวเอง) แต่เพื่อที่เมื่อฉันต้องการmyFunkyParserOperationและmyFunkyIOOperationฉันสามารถใช้โค้ดซ้ำที่ฉันเห็นในตอนแรกในแง่ของรายการได้เพราะมันเป็น monad-generic .

แต่หากต้องการนามธรรมในประเภทที่กำหนดพารามิเตอร์เช่น monad ที่มีความปลอดภัยคุณต้องมีประเภทที่สูงกว่า (รวมทั้งอธิบายไว้ในคำตอบอื่น ๆ ที่นี่)


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

"สิ่งที่คุณต้องการจริงๆคือนามธรรมเหล่านี้เพื่อให้คุณสามารถมีโค้ดที่ใช้งานได้ทั่วไปกับ functor / monad" F # มี monads ในรูปแบบของนิพจน์การคำนวณเมื่อ 13 ปีก่อนโดยเดิมเล่นกีฬา seq และ async monads วันนี้ F # สนุกกับโมนาดที่ 3 แบบสอบถาม ด้วย monads เพียงไม่กี่ตัวที่มีเหมือนกันเพียงเล็กน้อยทำไมคุณถึงต้องการนามธรรมมากกว่าพวกเขา?
JD

@JonHarrop คุณทราบอย่างชัดเจนว่ามีคนอื่นเขียนโค้ดโดยใช้ monads จำนวนมาก (และ functors ลูกศร ฯลฯ HKT ไม่ใช่แค่เรื่อง monads) ในภาษาที่รองรับ HKT และพบว่ามีการใช้เพื่อเป็นนามธรรมมากกว่า และเห็นได้ชัดว่าคุณไม่คิดว่าโค้ดนั้นจะใช้งานได้จริงและอยากรู้ว่าทำไมคนอื่น ๆ ถึงรำคาญที่จะเขียนมัน คุณคาดหวังว่าจะได้รับข้อมูลเชิงลึกประเภทใดจากการกลับมาถกเถียงกันในโพสต์อายุ 6 ขวบที่คุณเคยแสดงความคิดเห็นไว้เมื่อ 5 ปีก่อน
Ben

"หวังว่าจะได้รับโดยการกลับมาเริ่มการอภิปรายในโพสต์อายุ 6 ขวบ" ย้อนหลัง. ด้วยประโยชน์ของการมองย้อนกลับตอนนี้เราจึงรู้ว่านามธรรมของ F # ที่มีต่อ monads ส่วนใหญ่ยังไม่ได้ใช้ ดังนั้นความสามารถในการนามธรรมมากกว่า 3 สิ่งที่แตกต่างกันอย่างมากจึงเป็นเรื่องที่ไม่น่าสนใจ
JD

@JonHarrop ประเด็นของคำตอบของฉันคือ monads แต่ละตัว (หรือ functors หรืออื่น ๆ ) ไม่มีประโยชน์มากไปกว่าฟังก์ชันการทำงานที่คล้ายคลึงกันที่แสดงออกมาโดยไม่มีอินเทอร์เฟซแบบเร่ร่อน แต่การรวมสิ่งที่แตกต่างกันเข้าด้วยกันคือ ฉันจะยอมรับความเชี่ยวชาญของคุณเกี่ยวกับ F # แต่ถ้าคุณบอกว่ามีเพียง 3 monads (แทนที่จะใช้อินเทอร์เฟซแบบ monadic กับแนวคิดทั้งหมดที่อาจมีอย่างเดียวเช่นความล้มเหลวความเป็นรัฐการแยกวิเคราะห์ ฯลฯ ) ใช่ไม่น่าแปลกใจที่คุณจะไม่ได้รับประโยชน์มากนักจากการรวม 3 สิ่งนั้นเข้าด้วยกัน
เบ็น

15

สำหรับมุมมองเฉพาะของ. NET เพิ่มเติมฉันเขียนบล็อกโพสต์เกี่ยวกับเรื่องนี้มาแล้ว จุดสำคัญของมันคือด้วยประเภทที่สูงกว่าคุณสามารถนำบล็อก LINQ เดียวกันมาใช้ซ้ำระหว่างIEnumerablesและIObservablesแต่หากไม่มีประเภทที่สูงกว่านี้เป็นไปไม่ได้

ที่อยู่ใกล้คุณจะได้รับ (ฉันคิดออกหลังจากโพสต์บล็อก) คือการทำด้วยตัวเองIEnumerable<T>และและขยายพวกเขาทั้งสองจากIObservable<T> IMonad<T>สิ่งนี้จะช่วยให้คุณสามารถนำบล็อก LINQ ของคุณกลับมาใช้ใหม่ได้หากมีการแสดงIMonad<T>แต่จะไม่ปลอดภัยอีกต่อไปเพราะช่วยให้คุณสามารถผสมและจับคู่IObservablesและIEnumerablesอยู่ในบล็อกเดียวกันได้ซึ่งในขณะที่การเปิดใช้งานอาจฟังดูน่าสนใจ แต่คุณ โดยพื้นฐานแล้วเพียงแค่ได้รับพฤติกรรมที่ไม่ได้กำหนดบางอย่าง

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


2
ฉันจะให้ +1 เพื่อเป็นคำตอบเดียวที่กล่าวถึงสิ่งที่ใช้ได้จริง แต่ฉันไม่คิดว่าฉันเคยใช้IObservablesในรหัสการผลิต
JD

5
@JonHarrop นี่ดูเหมือนจะไม่จริง ใน F # เหตุการณ์ทั้งหมดคือIObservableและคุณใช้เหตุการณ์ในบท WinForms ของหนังสือของคุณเอง
Dax Fohl

1
ไมโครซอฟท์จ่ายเงินให้ฉันเขียนหนังสือเล่มนั้นและกำหนดให้ฉันปกปิดคุณลักษณะนั้น ฉันจำไม่ได้ว่าใช้เหตุการณ์ในรหัสการผลิต แต่ฉันจะดู
JD

การใช้ซ้ำระหว่าง IQueryable และ IEnumerable ก็เป็นไปได้เช่นกันฉันคิดว่า
KolA

สี่ปีต่อมาและฉันดูเสร็จแล้ว: เราถอด Rx ออกจากการผลิต
JD

13

ตัวอย่างที่ใช้กันมากที่สุดของความหลากหลายประเภทที่สูงกว่าใน Haskell คือMonadอินเทอร์เฟซ FunctorและApplicativeมีความกรุณาสูงกว่าในลักษณะเดียวกันดังนั้นฉันจะแสดงFunctorเพื่อแสดงสิ่งที่กระชับ

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

ตอนนี้ตรวจสอบคำจำกัดความนั้นดูว่าตัวแปรชนิดfถูกใช้อย่างไร คุณจะเห็นfว่าไม่ได้หมายถึงประเภทที่มีค่า คุณสามารถระบุค่าในลายเซ็นประเภทนั้นได้เนื่องจากเป็นอาร์กิวเมนต์และผลลัพธ์ของฟังก์ชัน ดังนั้นตัวแปรประเภทaและbประเภทที่สามารถมีค่าได้ นิพจน์ประเภทf aและf b. แต่ไม่ใช่fตัวมันเอง fเป็นตัวอย่างของตัวแปรประเภทสูงกว่า ระบุว่า*เป็นชนิดของประเภทที่สามารถมีค่าที่จะต้องมีชนิดf * -> *นั่นคือต้องใช้ประเภทที่สามารถมีค่าได้เพราะเรารู้จากการตรวจสอบก่อนหน้านี้ว่าaและbต้องมีค่า และเรายังรู้ว่าf aและf b ต้องมีค่าดังนั้นจึงส่งคืนชนิดที่ต้องมีค่า

สิ่งนี้ทำให้fใช้ในนิยามของFunctorตัวแปรประเภทที่สูงขึ้น

ApplicativeและMonadอินเตอร์เฟซที่เพิ่มมากขึ้น แต่พวกเขากำลังที่เข้ากันได้ ซึ่งหมายความว่าพวกเขาทำงานกับตัวแปรประเภทด้วยชนิด* -> *เช่นกัน

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


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