ฟังก์ชันไร้สาระใน Data.Void มีประโยชน์สำหรับอะไร?


97

absurdฟังก์ชั่นในData.Voidมีลายเซ็นต่อไปนี้ซึ่งVoidเป็นชนิดที่ไม่มีใครอยู่มีเหตุผลส่งออกโดยแพคเกจที่:

-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a

⊥ → aฉันจะรู้ตรรกะพอที่จะรับข้อสังเกตของเอกสารที่ตรงนี้โดยข้อเสนอตามที่ประเภทจดหมายสูตรที่ถูกต้อง

สิ่งที่ฉันงงงวยและสงสัยคือ: ฟังก์ชันนี้มีประโยชน์ในการเขียนโปรแกรมประเภทใดบ้าง ฉันคิดว่ามันอาจจะมีประโยชน์ในบางกรณีเนื่องจากเป็นวิธีที่ปลอดภัยในการจัดการกับกรณีที่ "ไม่สามารถเกิดขึ้น" ได้อย่างละเอียดถี่ถ้วน แต่ฉันไม่รู้เพียงพอเกี่ยวกับการใช้ Curry-Howard ในทางปฏิบัติเพื่อบอกว่าแนวคิดนั้นอยู่บน ถูกต้องเลย

แก้ไข: ตัวอย่างโดยเฉพาะอย่างยิ่งใน Haskell แต่ถ้าใครต้องการใช้ภาษาที่พิมพ์ขึ้นเองฉันจะไม่บ่น ...


5
การค้นหาอย่างรวดเร็วแสดงให้เห็นว่าabsurdมีการใช้ฟังก์ชันนี้ในบทความนี้เกี่ยวกับContmonad: haskellforall.com/2012/12/the-continuation-monad.html
Artyom

6
คุณสามารถดูabsurdเป็นทิศทางเดียวของมอร์ฟระหว่างและVoid forall a. a
Daniel Wagner

คำตอบ:


61

ชีวิตค่อนข้างยากเนื่องจาก Haskell ไม่เข้มงวด กรณีการใช้งานทั่วไปคือการจัดการกับเส้นทางที่เป็นไปไม่ได้ ตัวอย่างเช่น

simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y

สิ่งนี้จะมีประโยชน์อยู่บ้าง พิจารณาประเภทง่ายๆสำหรับPipes

data Pipe a b r
  = Pure r
  | Await (a -> Pipe a b r)
  | Yield !b (Pipe a b r)

นี่คือประเภทท่อมาตรฐานที่เข้มงวดและเรียบง่ายจากPipesไลบรารีของ Gabriel Gonzales ตอนนี้เราสามารถเข้ารหัสไปป์ที่ไม่ให้ผล (เช่นผู้บริโภค) เป็น

type Consumer a r = Pipe a Void r

สิ่งนี้ไม่เคยให้ผล ความหมายของสิ่งนี้คือกฎการพับที่เหมาะสมสำหรับ a Consumerคือ

foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p 
 = case p of
     Pure x -> onPure x
     Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
     Yield x _ -> absurd x

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

อาจเป็นการใช้งานแบบคลาสสิกที่สุดVoidใน CPS

type Continuation a = a -> Void

นั่นคือ a Continuationเป็นฟังก์ชันที่ไม่มีวันกลับมา Continuationเป็นเวอร์ชันประเภท "not" จากสิ่งนี้เราจะได้รับ monad ของ CPS (สอดคล้องกับตรรกะคลาสสิก)

newtype CPS a = Continuation (Continuation a)

เนื่องจาก Haskell บริสุทธิ์เราจึงไม่สามารถทำอะไรจากประเภทนี้ได้


1
ฉันสามารถทำตามบิต CPS นั้นได้จริงๆ ฉันเคยได้ยินเกี่ยวกับการโต้ตอบสองครั้งของ Curry-Howard / CPS มาก่อน แต่ไม่เข้าใจ ฉันจะไม่อ้างว่าฉันได้รับมันอย่างเต็มที่ในตอนนี้ แต่สิ่งนี้ช่วยได้อย่างแน่นอน!
Luis Casillas

5
@ErikAllik ในภาษาที่เข้มงวดVoidไม่มีใครอยู่ ใน Haskell ประกอบด้วย_|_. ในภาษาที่เข้มงวดVoidไม่สามารถใช้ตัวสร้างข้อมูลที่ใช้อาร์กิวเมนต์ประเภทได้ดังนั้นทางด้านขวามือของการจับคู่รูปแบบจึงไม่สามารถเข้าถึงได้ ใน Haskell คุณต้องใช้ a !เพื่อบังคับใช้และ GHC อาจไม่สังเกตว่าเส้นทางนั้นไม่สามารถเข้าถึงได้
dfeuer

1
agda คือโดยทั่วไปพูดทั้งหมดดังนั้นลำดับการประเมินจึงไม่สามารถสังเกตได้ ไม่มีคำปิด agda ของประเภทว่างเว้นแต่คุณจะปิดตัวตรวจสอบการสิ้นสุดหรืออะไรทำนองนั้น
Philip JF

1
@jcalz คำแนะนำ: แทนการหาสิ่งที่จะส่งผ่านไปยังอาร์กิวเมนต์ใช้เพื่อสร้างการทำงานของชนิดและหน้าที่ของประเภทa -> Void (a -> Void) -> Void
PyRulez

1
@PyRulez ขอบคุณ! คำตอบดูชัดเจนมากเมื่อมองย้อนกลับไปจนฉันมองไม่เห็นว่าทำไมถึงพลาดไปก่อนหน้านี้
jcalz

57

พิจารณาการแทนค่านี้สำหรับคำศัพท์แลมบ์ดาที่กำหนดโดยตัวแปรอิสระ (ดูเอกสารของ Bellegarde and Hook 1994, Bird and Paterson 1999, Altenkirch และ Reus 1999)

data Tm a  = Var a
           | Tm a :$ Tm a
           | Lam (Tm (Maybe a))

คุณสามารถสร้างสิ่งนี้ได้อย่างแน่นอนFunctorโดยจับความคิดของการเปลี่ยนชื่อและMonadจับความคิดของการแทนที่

instance Functor Tm where
  fmap rho (Var a)   = Var (rho a)
  fmap rho (f :$ s)  = fmap rho f :$ fmap rho s
  fmap rho (Lam t)   = Lam (fmap (fmap rho) t)

instance Monad Tm where
  return = Var
  Var a     >>= sig  = sig a
  (f :$ s)  >>= sig  = (f >>= sig) :$ (s >>= sig)
  Lam t     >>= sig  = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))

ตอนนี้พิจารณา ปิดTm Voidเงื่อนไขเหล่านี้เป็นที่อาศัยอยู่ใน คุณควรจะสามารถฝังคำที่ปิดลงในเงื่อนไขที่มีตัวแปรอิสระตามอำเภอใจ อย่างไร?

fmap absurd :: Tm Void -> Tm a

แน่นอนว่าสิ่งที่จับได้คือฟังก์ชันนี้จะข้ามผ่านคำที่ทำอะไรไม่ได้อย่างแม่นยำ unsafeCoerceแต่มันเป็นสัมผัสเที่ยงตรงมากกว่า และนั่นคือสาเหตุที่vacuousถูกเพิ่มเข้าไปในData.Void...

หรือเขียนผู้ประเมิน. นี่คือค่าที่มีตัวแปรอิสระในb .

data Val b
  =  b :$$ [Val b]                              -- a stuck application
  |  forall a. LV (a -> Val b) (Tm (Maybe a))   -- we have an incomplete environment

ฉันเพิ่งเป็นตัวแทนของ lambdas เป็นปิด ประเมินเป็น parametrized โดยตัวแปรอิสระทำแผนที่สภาพแวดล้อมในการค่ามากกว่าab

eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a)   = g a
eval g (f :$ s)  = eval g f $$ eval g s where
  (b :$$ vs)  $$ v  = b :$$ (vs ++ [v])         -- stuck application gets longer
  LV g t      $$ v  = eval (maybe v g) t        -- an applied lambda gets unstuck
eval g (Lam t)   = LV g t

คุณเดาได้ เพื่อประเมินระยะปิดที่เป้าหมายใด ๆ

eval absurd :: Tm Void -> Val b

โดยทั่วไปแล้วมักVoidไม่ค่อยมีการใช้ด้วยตัวเอง แต่มีประโยชน์เมื่อคุณต้องการสร้างอินสแตนซ์พารามิเตอร์ประเภทในลักษณะที่บ่งบอกถึงความเป็นไปไม่ได้บางประเภท (เช่นที่นี่ใช้ตัวแปรอิสระในระยะปิด) บ่อยครั้งที่ประเภท parametrized เหล่านี้มาพร้อมกับฟังก์ชันลำดับที่สูงขึ้นในการยกการดำเนินการของพารามิเตอร์ไปสู่การดำเนินการกับทั้งประเภท (เช่นที่นี่fmap,>>= , eval) ดังนั้นคุณผ่านในฐานะการดำเนินงานทั่วไปที่มีวัตถุประสงค์ในการabsurdVoid

สำหรับตัวอย่างอื่นจินตนาการโดยใช้Either e vการคำนวณจับซึ่งหวังว่าให้คุณแต่อาจจะเพิ่มข้อยกเว้นชนิดv eคุณอาจใช้วิธีนี้ในการบันทึกความเสี่ยงของพฤติกรรมที่ไม่ดีอย่างสม่ำเสมอ สำหรับการคำนวณที่ทำงานได้ดีอย่างสมบูรณ์แบบในการตั้งค่านี้eให้เป็นVoidใช้

either absurd id :: Either Void v -> v

เพื่อให้ทำงานได้อย่างปลอดภัยหรือ

either absurd Right :: Either Void v -> Either e v

เพื่อฝังส่วนประกอบที่ปลอดภัยในโลกที่ไม่ปลอดภัย

โอ้และอีกครั้งสุดท้ายที่รีบจัดการกับ "ไม่สามารถเกิดขึ้นได้" มันปรากฏขึ้นในโครงสร้างซิปทั่วไปทุกที่ที่เคอร์เซอร์ไม่สามารถอยู่ได้

class Differentiable f where
  type D f :: * -> *              -- an f with a hole
  plug :: (D f x, x) -> f x       -- plugging a child in the hole

newtype K a     x  = K a          -- no children, just a label
newtype I       x  = I x          -- one child
data (f :+: g)  x  = L (f x)      -- choice
                   | R (g x)
data (f :*: g)  x  = f x :&: g x  -- pairing

instance Differentiable (K a) where
  type D (K a) = K Void           -- no children, so no way to make a hole
  plug (K v, x) = absurd v        -- can't reinvent the label, so deny the hole!

ฉันตัดสินใจที่จะไม่ลบส่วนที่เหลือแม้ว่าจะไม่ตรงประเด็นก็ตาม

instance Differentiable I where
  type D I = K ()
  plug (K (), x) = I x

instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
  type D (f :+: g) = D f :+: D g
  plug (L df, x) = L (plug (df, x))
  plug (R dg, x) = R (plug (dg, x))

instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
  type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
  plug (L (df :&: g), x) = plug (df, x) :&: g
  plug (R (f :&: dg), x) = f :&: plug (dg, x)

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

data Term f x = Var x | Con (f (Term f x))   -- the Free monad, yet again

ในไวยากรณ์ใด ๆ ที่สร้างได้อย่างอิสระจากDifferentiableและfunctorTraversable fเราใช้Term f Voidเพื่อแสดงภูมิภาคที่ไม่มีตัวแปรอิสระและ[D f (Term f Void)]เป็นตัวแทนท่อขุดเจาะอุโมงค์ผ่านพื้นที่ที่มีไม่มีตัวแปรอิสระทั้งตัวแปรอิสระแยกหรือทางแยกในเส้นทางถึงสองหรือตัวแปรอิสระมากขึ้น ต้องจบบทความนั้นในบางครั้ง

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


จะforall f. vacuous f = unsafeCoerce fเป็นกฎการเขียนซ้ำ GHC ที่ถูกต้องหรือไม่
Cactus

1
@ แคคตัสไม่จริงหรอก Functorอินสแตนซ์ปลอมอาจเป็น GADT ที่ไม่ได้มีอะไรเหมือนกับ functors
dfeuer

พวกนั้นจะFunctorไม่ผิดfmap id = idกฎหรือ? หรือนั่นคือสิ่งที่คุณหมายถึง "ปลอม" ที่นี่?
Cactus

34

ฉันคิดว่าอาจมีประโยชน์ในบางกรณีเนื่องจากเป็นวิธีจัดการกรณี "ไม่สามารถเกิดขึ้น" ได้อย่างปลอดภัย

นี่คือสิ่งที่ถูกต้อง

คุณอาจจะบอกว่าไม่เป็นประโยชน์มากกว่าabsurd const (error "Impossible")อย่างไรก็ตามประเภทนี้ถูก จำกัด เพื่อให้อินพุตเพียงอย่างเดียวสามารถเป็นบางประเภทได้Voidซึ่งเป็นประเภทข้อมูลที่ถูกปล่อยให้ไม่มีใครอยู่โดยเจตนา ซึ่งหมายความว่าไม่มีค่าที่แท้จริงที่คุณสามารถส่งผ่านไปabsurdได้ หากคุณเคยลงท้ายด้วยสาขาของรหัสที่ตัวตรวจสอบประเภทคิดว่าคุณสามารถเข้าถึงบางสิ่งบางอย่างได้แสดงVoidว่าคุณอยู่ในสถานการณ์ที่ไร้สาระ ดังนั้นคุณเพียงแค่ใช้absurdเพื่อทำเครื่องหมายว่าไม่ควรเข้าถึงสาขาของรหัสนี้

"Ex falso quodlibet" ตามตัวอักษรแปลว่า "จาก [a] เท็จ [ประพจน์] สิ่งต่อไปนี้" ดังนั้นเมื่อคุณพบว่าคุณถือข้อมูลที่เป็นประเภทVoidคุณก็รู้ว่าคุณมีหลักฐานเท็จอยู่ในมือ คุณจึงสามารถเติมอะไรก็ได้คุณต้องการ (ผ่านabsurd) เพราะจากเรื่องเท็จจะมีอะไรตามมา

absurdผมเขียนบล็อกโพสต์เกี่ยวกับความคิดที่อยู่เบื้องหลังท่อซึ่งมีตัวอย่างของการใช้

http://unknownparallel.wordpress.com/2012/07/30/pipes-to-conduits-part-6-leftovers/#running-a-pipeline


13

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

data RuleSet a            = Known !a | Unknown String
data GoRuleChoices        = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet            = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices

จากนั้นคุณสามารถใช้absurdสิ่งนี้เช่น:

handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
    Known   a -> absurd a
    Unknown s -> f s

13

มีวิธีการที่แตกต่างกันวิธีการแทนเป็นชนิดข้อมูลที่ว่างเปล่า หนึ่งคือประเภทข้อมูลเกี่ยวกับพีชคณิตที่ว่างเปล่า อีกวิธีหนึ่งคือทำให้เป็นนามแฝงสำหรับ∀α.αหรือ

type Void' = forall a . a

ใน Haskell - นี่คือวิธีที่เราสามารถเข้ารหัสในระบบ F (ดูบทที่ 11 ของการพิสูจน์และประเภท ) คำอธิบายทั้งสองนี้แน่นอนว่าเป็นไอโซมอร์ฟิกและไอโซมอร์ฟิซึมนั้นเป็นที่ประจักษ์โดย\x -> x :: (forall a.a) -> Voidและโดยabsurd :: Void -> aและ

ในบางกรณีเราชอบตัวแปรที่ชัดเจนโดยปกติถ้าประเภทข้อมูลว่างปรากฏในอาร์กิวเมนต์ของฟังก์ชันหรือในประเภทข้อมูลที่ซับซ้อนกว่าเช่นในData.Conduit :

type Sink i m r = Pipe i i Void () m r

ในบางกรณีเราชอบตัวแปรความหลากหลายโดยปกติแล้วประเภทข้อมูลว่างจะเกี่ยวข้องกับประเภทการส่งคืนของฟังก์ชัน

absurd เกิดขึ้นเมื่อเรากำลังแปลงระหว่างการแสดงทั้งสองนี้


ยกตัวอย่างเช่นcallcc :: ((a -> m b) -> m a) -> m aการใช้งาน forall b(โดยปริยาย) อาจเป็นประเภทได้เช่นกัน((a -> m Void) -> m a) -> m aเนื่องจากการเรียกไปยังทวีปไม่ได้ส่งคืนจริงเป็นการโอนการควบคุมไปยังจุดอื่น หากเราต้องการทำงานกับความต่อเนื่องเราสามารถกำหนดได้

type Continuation r a = a -> Cont r Void

(เราสามารถใช้type Continuation' r a = forall b . a -> Cont r bแต่ที่จะต้องมีการจัดอันดับ 2 ประเภท.) และจากนั้นvacuousMแปลงนี้เข้าไปCont r VoidCont r b

(โปรดทราบว่าคุณสามารถใช้haskellers.comเพื่อค้นหาการใช้งาน (การอ้างอิงย้อนกลับ) ของแพ็กเกจบางแพ็กเกจเช่นดูว่าใครใช้แพ็กเกจโมฆะได้อย่างไร)


TypeApplicationsสามารถนำมาใช้ให้มากขึ้นอย่างชัดเจนเกี่ยวกับรายละเอียดของ:proof :: (forall a. a) -> Void proof fls = fls @Void
Iceland_jack

1

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

ตัวอย่างเช่นฟังก์ชันนี้จะลบองค์ประกอบออกจากรายการที่มี costraint ระดับชนิดที่มีอยู่:

shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p

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

ผ่านมุมมองของ Curry-Howard ซึ่งเป็นข้อเสนอจากนั้นabsurdจึงจัดเรียง QED ในการพิสูจน์โดยความขัดแย้ง

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