Chức năng vô lý trong Data.Void hữu ích cho điều gì?


97

Các absurdchức năng trong Data.Voidcó chữ ký dưới đây, nơi Voidlà loại logic không có người ở xuất khẩu bằng cách gói:

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

Tôi biết đủ logic để có được nhận xét của tài liệu rằng điều này tương ứng, theo sự tương ứng của mệnh đề-as-type, với công thức hợp lệ ⊥ → a.

Điều tôi phân vân và tò mò là: chức năng này hữu ích trong những vấn đề lập trình thực tế nào? Tôi nghĩ rằng có lẽ nó hữu ích trong một số trường hợp như một cách an toàn kiểu loại để xử lý thấu đáo các trường hợp "không thể xảy ra", nhưng tôi không biết đủ về các ứng dụng thực tế của Curry-Howard để biết liệu ý tưởng đó có phù hợp với đi đúng hướng.

CHỈNH SỬA: Tốt nhất là các ví dụ trong Haskell, nhưng nếu ai đó muốn sử dụng ngôn ngữ được đánh máy phụ thuộc, tôi sẽ không phàn nàn ...


5
Một cách nhanh chóng các chương trình tìm kiếm mà absurdchức năng đã được sử dụng trong giao dịch bài viết này với các Contđơn nguyên: haskellforall.com/2012/12/the-continuation-monad.html
Artyom

6
Bạn có thể xem đây absurdlà một hướng của sự đẳng cấu giữa Voidforall a. a.
Daniel Wagner

Câu trả lời:


61

Cuộc sống có một chút khó khăn, vì Haskell không nghiêm khắc. Trường hợp sử dụng chung là xử lý các đường dẫn không thể. Ví dụ

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

Điều này hóa ra có phần hữu ích. Hãy xem xét một loại đơn giản choPipes

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

đây là phiên bản được đơn giản hóa và nghiêm ngặt của loại đường ống tiêu chuẩn từ Pipesthư viện của Gabriel Gonzales . Bây giờ, chúng tôi có thể mã hóa một đường ống không bao giờ mang lại (tức là người tiêu dùng) là

type Consumer a r = Pipe a Void r

điều này thực sự không bao giờ mang lại. Hàm ý của điều này là quy tắc gấp thích hợp cho 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

hoặc cách khác, bạn có thể bỏ qua trường hợp lợi nhuận khi giao dịch với người tiêu dùng. Đây là phiên bản chung của mẫu thiết kế này: sử dụng các kiểu dữ liệu đa hình và Voidloại bỏ các khả năng khi bạn cần.

Có lẽ cách sử dụng cổ điển nhất Voidlà trong CPS.

type Continuation a = a -> Void

nghĩa là, a Continuationlà một hàm không bao giờ trả về. Continuationlà phiên bản loại của "not." Từ đó chúng ta nhận được một đơn nguyên của CPS (tương ứng với logic cổ điển)

newtype CPS a = Continuation (Continuation a)

vì Haskell là nguyên chất, chúng ta không thể lấy được gì từ loại này.


1
Huh, tôi thực sự có thể theo dõi bit CPS đó. Tôi chắc chắn đã nghe nói về sự phủ định kép của Curry-Howard / tương ứng CPS trước đây, nhưng không hiểu nó; Tôi sẽ không tuyên bố rằng tôi hoàn toàn có được nó ngay bây giờ, nhưng điều này chắc chắn có ích!
Luis Casillas

"Cuộc sống có một chút khó khăn, vì Haskell không nghiêm khắc " - chính xác thì bạn muốn nói gì về điều đó?
Erik Kaplun

4
@ErikAllik, trong một ngôn ngữ nghiêm ngặt, Voidkhông có người ở. Trong Haskell, nó có chứa _|_. Trong một ngôn ngữ nghiêm ngặt, một phương thức khởi tạo dữ liệu nhận đối số kiểu Voidkhông bao giờ có thể được áp dụng, vì vậy không thể truy cập phía bên phải của đối sánh mẫu. Trong Haskell, bạn cần sử dụng một !để thực thi điều đó và GHC có thể sẽ không nhận thấy rằng đường dẫn không thể truy cập được.
dfeuer

Agda thì sao? nó lười biếng nhưng nó có _|_? và nó có bị cùng một giới hạn sau đó không?
Erik Kaplun

Nói chung, agda là tổng số và do đó, thứ tự đánh giá không thể quan sát được. Không có hạn agda khép kín của các loại sản phẩm nào, trừ khi bạn tắt trình kiểm tra chấm dứt hoặc một cái gì đó như thế
Philip JF

58

Hãy xem xét biểu diễn này cho các thuật ngữ lambda được tham số hóa bởi các biến tự do của chúng. (Xem các bài báo của Bellegarde và Hook 1994, Bird và Paterson 1999, Altenkirch và Reus 1999.)

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

Bạn chắc chắn có thể biến điều này thành một Functor, nắm bắt khái niệm đổi tên và Monadnắm bắt khái niệm thay thế.

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

Bây giờ hãy xem xét các điều khoản đã đóng : đây là những cư dân của Tm Void. Bạn có thể nhúng các điều khoản đã đóng vào các điều khoản với các biến tự do tùy ý. Làm sao?

fmap absurd :: Tm Void -> Tm a

Tất nhiên, điểm bắt buộc là hàm này sẽ đi ngang qua thuật ngữ mà chính xác là không làm gì cả. Nhưng đó là một liên lạc trung thực hơn unsafeCoerce. Và đó là lý do tại sao vacuousđược thêm vào Data.Void...

Hoặc viết một người đánh giá. Đây là các giá trị với các biến miễn phí trong b.

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

Tôi vừa đại diện cho lambdas dưới dạng kết thúc. Bộ đánh giá được tham số hóa bởi một môi trường ánh xạ các biến tự do vào acác giá trị hơn b.

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

Bạn đoán nó. Để đánh giá một kỳ hạn đã đóng ở bất kỳ mục tiêu nào

eval absurd :: Tm Void -> Val b

Nói chung, Voidhiếm khi được sử dụng riêng, nhưng rất tiện lợi khi bạn muốn khởi tạo một tham số kiểu theo cách chỉ ra một số loại bất khả thi (ví dụ: ở đây, sử dụng một biến tự do trong một thuật ngữ đóng). Thông thường các loại parametrized đi kèm với chức năng bậc cao nâng hoạt động trên các thông số để hoạt động trên toàn bộ loại (ví dụ, ở đây, fmap, >>=, eval). Vì vậy, bạn chuyển absurdnhư là hoạt động mục đích chung Void.

Ví dụ khác, hãy tưởng tượng việc sử dụng Either e vđể nắm bắt các phép tính hy vọng cung cấp cho bạn vnhưng có thể đưa ra một ngoại lệ về kiểu e. Bạn có thể sử dụng phương pháp này để ghi nhận rủi ro hành vi xấu một cách thống nhất. Đối với một cách hoàn hảo cư xử rất tốt tính toán trong bối cảnh này, hãy etrở thành Void, sau đó sử dụng

either absurd id :: Either Void v -> v

để chạy một cách an toàn hoặc

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

để nhúng các thành phần an toàn vào một thế giới không an toàn.

Ồ, và một lần cuối cùng, xử lý "không thể xảy ra". Nó hiển thị trong cấu trúc dây kéo chung, ở mọi nơi mà con trỏ không thể có.

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!

Tôi quyết định không xóa phần còn lại, mặc dù nó không liên quan chính xác.

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)

Trên thực tế, có lẽ nó có liên quan. Nếu bạn đang cảm thấy mạo hiểm, bài viết chưa hoàn thành này chỉ ra cách sử dụng Voidđể nén biểu diễn của các thuật ngữ với các biến tự do

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

trong bất kỳ cú pháp nào được tạo tự do từ a DifferentiableTraversablefunctor f. Chúng tôi sử dụng Term f Voidđể đại diện cho các vùng không có biến tự do và [D f (Term f Void)]để biểu diễn các ống chui qua các vùng không có biến tự do hoặc đến một biến tự do cô lập hoặc đến một điểm giao nhau trong đường dẫn đến hai hoặc nhiều biến tự do. Một lúc nào đó phải kết thúc bài báo đó.

Đối với một loại không có giá trị (hoặc ít nhất, không có giá trị nào đáng nói trong một công ty lịch sự), Voidrất hữu ích. Và absurdlà cách bạn sử dụng nó.


Sẽ forall f. vacuous f = unsafeCoerce flà một quy tắc viết lại GHC hợp lệ?
Cactus

1
@Cactus, không hẳn vậy. Các Functortrường hợp Bogus có thể là GADT không thực sự giống như các trình điều khiển.
dfeuer

Những điều đó Functorsẽ không phá vỡ fmap id = idquy tắc chứ? Hay đó là những gì bạn có nghĩa là "không có thật" ở đây?
Cactus

35

Tôi nghĩ rằng có lẽ nó hữu ích trong một số trường hợp như một cách an toàn kiểu loại để xử lý thấu đáo các trường hợp "không thể xảy ra"

Điều này hoàn toàn đúng.

Bạn có thể nói rằng điều đó absurdkhông hữu ích hơnconst (error "Impossible") . Tuy nhiên, nó bị hạn chế về kiểu, do đó đầu vào duy nhất của nó có thể là kiểu gì đó Void, kiểu dữ liệu được cố ý không có người ở. Điều này có nghĩa là không có giá trị thực tế nào mà bạn có thể chuyển đến absurd. Nếu bạn kết thúc với một nhánh mã mà trình kiểm tra kiểu nghĩ rằng bạn có quyền truy cập vào một thứ gì đó thuộc kiểu Void, thì, bạn đang ở trong một tình huống vô lý . Vì vậy, bạn chỉ cần sử dụng absurdđể đánh dấu về cơ bản rằng nhánh mã này sẽ không bao giờ được tiếp cận.

"Ex falso quodlibet" có nghĩa đen là "từ [a] sai [mệnh đề], bất cứ điều gì theo sau". Vì vậy, khi bạn nhận thấy rằng bạn đang nắm giữ một phần dữ liệu có loại dữ liệu Void, bạn biết rằng bạn có bằng chứng giả trong tay. Do đó, bạn có thể lấp đầy bất kỳ lỗ hổng nào bạn muốn (thông qua absurd), bởi vì từ một mệnh đề sai, bất kỳ điều gì sẽ theo sau.

Tôi đã viết một bài đăng trên blog về những ý tưởng đằng sau Conduit có một ví dụ về cách sử dụng absurd.

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


13

Nói chung, bạn có thể sử dụng nó để tránh các mẫu trùng khớp rõ ràng từng phần. Ví dụ: lấy một ước tính của các khai báo kiểu dữ liệu từ câu trả lời này :

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

Sau đó, bạn có thể sử dụng absurdnhư thế này, ví dụ:

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

13

Có nhiều cách khác nhau để biểu diễn kiểu dữ liệu trống . Một là kiểu dữ liệu đại số trống. Một cách khác là đặt nó làm bí danh cho ∀α.α hoặc

type Void' = forall a . a

trong Haskell - đây là cách chúng ta có thể mã hóa nó trong Hệ thống F (xem Chương 11 của Chứng minh và Loại ). Hai mô tả này tất nhiên là đẳng cấu và đẳng cấu được chứng kiến ​​bởi \x -> x :: (forall a.a) -> Voidvà bởiabsurd :: Void -> a .

Trong một số trường hợp, chúng tôi thích biến thể rõ ràng, thường là nếu kiểu dữ liệu trống xuất hiện trong đối số của một hàm hoặc trong một kiểu dữ liệu phức tạp hơn, chẳng hạn như trong Data.Conduit :

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

Trong một số trường hợp, chúng tôi thích biến thể đa hình hơn, thường thì kiểu dữ liệu trống có liên quan đến kiểu trả về của một hàm.

absurd phát sinh khi chúng tôi chuyển đổi giữa hai cách biểu diễn này.


Ví dụ, callcc :: ((a -> m b) -> m a) -> m asử dụng (ngầm định) forall b. Nó cũng có thể thuộc loại này ((a -> m Void) -> m a) -> m a, bởi vì một lệnh gọi tới contination không thực sự trở lại, nó chuyển quyền kiểm soát sang một điểm khác. Nếu chúng tôi muốn làm việc với sự liên tục, chúng tôi có thể xác định

type Continuation r a = a -> Cont r Void

(Chúng tôi có thể sử dụng type Continuation' r a = forall b . a -> Cont r bnhưng điều đó sẽ yêu cầu loại xếp hạng 2.) Và sau đó, vacuousMchuyển đổi điều này Cont r Voidthành Cont r b.

(Cũng lưu ý rằng bạn có thể sử dụng haskellers.com để tìm kiếm cách sử dụng (phụ thuộc ngược) của một gói nhất định, như để xem ai và cách sử dụng gói void .)


TypeApplicationscó thể được sử dụng để được rõ ràng hơn về chi tiết của proof :: (forall a. a) -> Void: proof fls = fls @Void.
Iceland_jack

1

Trong các ngôn ngữ được đánh máy phụ thuộc như Idris, nó có lẽ hữu ích hơn trong Haskell. Thông thường, trong một hàm tổng số khi bạn đặt mẫu khớp với một giá trị mà thực sự không thể được đưa vào hàm, thì bạn sẽ tạo một giá trị thuộc loại không có và sử dụngabsurd để hoàn thiện định nghĩa chữ hoa chữ thường.

Ví dụ: hàm này loại bỏ một phần tử khỏi danh sách với chi phí cấp loại mà nó hiện diện ở đó:

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

Trường hợp thứ hai nói rằng có một phần tử nào đó trong một danh sách trống, điều này thật vô lý. Tuy nhiên, nói chung, trình biên dịch không biết điều này và chúng tôi thường phải rõ ràng. Sau đó, trình biên dịch có thể kiểm tra rằng định nghĩa hàm không phải là một phần và chúng tôi có được đảm bảo thời gian biên dịch mạnh hơn.

Theo quan điểm của Curry-Howard, đâu là mệnh đề, thì absurdQED được coi là một bằng chứng mâu thuẫn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.