Zipper Comonads, Nói chung


80

Với bất kỳ loại vùng chứa nào, chúng ta có thể tạo Zipper (tập trung vào phần tử) và biết rằng cấu trúc này là một Dấu phẩy. Điều này gần đây đã được khám phá chi tiết tuyệt vời trong một câu hỏi Stack Overflow khác cho loại sau:

data Bin a = Branch (Bin a) a (Bin a) | Leaf a deriving Functor

với dây kéo sau

data Dir = L | R
data Step a = Step a Dir (Bin a)   deriving Functor
data Zip  a = Zip [Step a] (Bin a) deriving Functor
instance Comonad Zip where ...

Đây là trường hợp đó Ziplà một Comonadmặc dù việc xây dựng các ví dụ của nó là một lông nhỏ. Điều đó nói rằng, Zipcó thể hoàn toàn được bắt nguồn từ một cách máy móc Treevà (tôi tin rằng) bất kỳ kiểu nào bắt nguồn theo cách này đều tự động là a Comonad, vì vậy tôi cảm thấy trường hợp đó chúng ta có thể xây dựng các kiểu này và các kết hợp của chúng một cách chung chung và tự động.

Một phương pháp để đạt được tính tổng quát cho việc xây dựng dây kéo là sử dụng họ loại và lớp sau

data Zipper t a = Zipper { diff :: D t a, here :: a }

deriving instance Diff t => Functor (Zipper t)

class (Functor t, Functor (D t)) => Diff t where
  data D t :: * -> *
  inTo  :: t a -> t (Zipper t a)
  outOf :: Zipper t a -> t a

đã (ít nhiều) hiển thị trong chuỗi Haskell Cafe và trên blog của Conal Elliott. Lớp này có thể được khởi tạo cho các kiểu đại số cốt lõi khác nhau và do đó cung cấp một khuôn khổ chung để nói về các dẫn xuất của ADT.

Vì vậy, cuối cùng, câu hỏi của tôi là liệu chúng ta có thể viết

instance Diff t => Comonad (Zipper t) where ...

có thể được sử dụng để đánh số cá thể Comonad cụ thể được mô tả ở trên:

instance Diff Bin where
  data D Bin a = DBin { context :: [Step a], descend :: Maybe (Bin a, Bin a) }
  ...

Thật không may, tôi đã không có may mắn khi viết một ví dụ như vậy. Là inTo/ outOfchữ ký đầy đủ? Có điều gì khác cần thiết để hạn chế các loại không? Liệu trường hợp này có khả thi không?


29
Cho chúng tôi một phút ...
pigworker

Bạn có tài liệu tham khảo cho việc triển khai Difffor Either(,)? Tôi có một giải pháp khả thi vô cùng đơn giản mà tôi muốn kiểm tra.
Cirdec

@Cirdec Bạn không nhất thiết muốn triển khai nó cho Either mà thay vào đó cho Either1 f g x = Inl (f x) | Inr (g x). Blog của Conal có đầy đủ chi tiết.
J. Abrahamson

Trên thực tế, Eitherkhông hoàn toàn có thể được triển khai trong khuôn khổ này (và hy vọng một câu trả lời thực sự cho câu hỏi này sẽ giải quyết vấn đề này) vì Zippergiả sử bạn có thể trỏ đến ít nhất một giá trị đích . Thực ra, điều này không thể xảy ra đối với các loại có thể là "trống".
J. Abrahamson

3
@Patrick Câu hỏi này thực sự khá chính xác, mặc dù nó dựa trên các tính năng Haskell khá tiên tiến. Và câu trả lời cuối cùng của Cirdec không dài như vậy. Người thợ lợn đó có thói quen làm cho câu trả lời của mình rất kỹ lưỡng là một vấn đề khác, mà hầu hết mọi người đều đánh giá cao.
Ørjan Johansen

Câu trả lời:


113

Giống như người bắt trẻ trong Chitty-Chitty-Bang-Bang dụ trẻ em bị giam cầm bằng đồ ngọt và đồ chơi, những người tuyển dụng vào ngành Vật lý bậc đại học thích đánh lừa bằng bong bóng xà phòng và boomerang, nhưng khi cánh cửa đóng lại, đó là "Đúng rồi, các em, đã đến lúc học về phân hóa từng phần! ”. Tôi cũng vậy. Đừng nói rằng tôi đã không cảnh báo bạn.

Đây là một cảnh báo khác: mã sau đây cần {-# LANGUAGE KitchenSink #-}, hay đúng hơn là

{-# LANGUAGE TypeFamilies, FlexibleContexts, TupleSections, GADTs, DataKinds,
    TypeOperators, FlexibleInstances, RankNTypes, ScopedTypeVariables,
    StandaloneDeriving, UndecidableInstances #-}

không theo thứ tự đặc biệt.

Các bộ chức năng khác biệt có khóa kéo hài hòa

Dù sao thì một functor có thể phân biệt được là gì?

class (Functor f, Functor (DF f)) => Diff1 f where
  type DF f :: * -> *
  upF      ::  ZF f x  ->  f x
  downF    ::  f x     ->  f (ZF f x)
  aroundF  ::  ZF f x  ->  ZF f (ZF f x)

data ZF f x = (:<-:) {cxF :: DF f x, elF :: x}

Đó là một functor có dẫn xuất, cũng là một functor. Đạo hàm đại diện cho ngữ cảnh một lỗ cho một phần tử . Loại dây kéo ZF f xđại diện cho cặp bối cảnh một lỗ và phần tử trong lỗ.

Các thao tác để Diff1mô tả các loại điều hướng mà chúng ta có thể thực hiện trên dây kéo (không có bất kỳ khái niệm nào về "sang trái" và "sang phải", hãy xem bài báo Clown and Jokers của tôi ). Chúng ta có thể đi lên "phía trên", lắp ráp lại cấu trúc bằng cách cắm phần tử vào lỗ của nó. Chúng ta có thể "đi xuống", tìm mọi cách để truy cập một phần tử trong cấu trúc cho trước: chúng tôi trang trí mọi phần tử với ngữ cảnh của nó. Chúng tôi có thể đi "xung quanh", lấy một dây kéo hiện có và trang trí từng yếu tố với bối cảnh của nó, vì vậy chúng tôi tìm mọi cách để lấy lại sự tập trung (và cách giữ trọng tâm hiện tại của chúng tôi).

Bây giờ, loại aroundFcó thể nhắc nhở một số bạn về

class Functor c => Comonad c where
  extract    :: c x -> x
  duplicate  :: c x -> c (c x)

và bạn đúng khi được nhắc nhở! Chúng tôi có, với một bước nhảy và một lần bỏ qua,

instance Diff1 f => Functor (ZF f) where
  fmap f (df :<-: x) = fmap f df :<-: f x

instance Diff1 f => Comonad (ZF f) where
  extract    = elF
  duplicate  = aroundF

và chúng tôi nhấn mạnh rằng

extract . duplicate == id
fmap extract . duplicate == id
duplicate . duplicate == fmap duplicate . duplicate

Chúng tôi cũng cần điều đó

fmap extract (downF xs) == xs              -- downF decorates the element in position
fmap upF (downF xs) = fmap (const xs) xs   -- downF gives the correct context

Hàm đa thức có thể phân biệt được

Các chức năng liên tục có thể phân biệt được.

data KF a x = KF a
instance Functor (KF a) where
  fmap f (KF a) = KF a

instance Diff1 (KF a) where
  type DF (KF a) = KF Void
  upF (KF w :<-: _) = absurd w
  downF (KF a) = KF a
  aroundF (KF w :<-: _) = absurd w

Không có nơi nào để đặt một phần tử, vì vậy không thể tạo bối cảnh. Không có nơi nào để đi upFhoặc về downF, và chúng tôi dễ dàng tìm thấy tất cả các con đường để đi downF.

Chức năng nhận dạng có thể phân biệt được.

data IF x = IF x
instance Functor IF where
  fmap f (IF x) = IF (f x)

instance Diff1 IF where
  type DF IF = KF ()
  upF (KF () :<-: x) = IF x
  downF (IF x) = IF (KF () :<-: x)
  aroundF z@(KF () :<-: x) = KF () :<-: z

Có một yếu tố trong một bối cảnh tầm thường, downFtìm nó, đóng upFgói lại và aroundFchỉ có thể giữ nguyên.

Tính tổng bảo toàn tính khác biệt.

data (f :+: g) x = LF (f x) | RF (g x)
instance (Functor f, Functor g) => Functor (f :+: g) where
  fmap h (LF f) = LF (fmap h f)
  fmap h (RF g) = RF (fmap h g)

instance (Diff1 f, Diff1 g) => Diff1 (f :+: g) where
  type DF (f :+: g) = DF f :+: DF g
  upF (LF f' :<-: x) = LF (upF (f' :<-: x))
  upF (RF g' :<-: x) = RF (upF (g' :<-: x))

Các bit và mảnh khác là một số ít. Để bắt đầu downF, chúng ta phải vào downFbên trong thành phần được gắn thẻ, sau đó sửa các khóa kéo kết quả để hiển thị thẻ trong ngữ cảnh.

  downF (LF f) = LF (fmap (\ (f' :<-: x) -> LF f' :<-: x) (downF f))
  downF (RF g) = RF (fmap (\ (g' :<-: x) -> RF g' :<-: x) (downF g))

Để bắt đầu aroundF, chúng tôi tách thẻ, tìm ra cách đi vòng quanh thứ không được gắn thẻ, sau đó khôi phục thẻ trong tất cả các khóa kéo kết quả. Yếu tố được chú trọng, xđược thay thế bằng toàn bộ dây kéo của nó , z.

  aroundF z@(LF f' :<-: (x :: x)) =
    LF (fmap (\ (f' :<-: x) -> LF f' :<-: x) . cxF $ aroundF (f' :<-: x :: ZF f x))
    :<-: z
  aroundF z@(RF g' :<-: (x :: x)) =
    RF (fmap (\ (g' :<-: x) -> RF g' :<-: x) . cxF $ aroundF (g' :<-: x :: ZF g x))
    :<-: z

Lưu ý rằng tôi đã phải sử dụng ScopedTypeVariablesđể phân biệt các cuộc gọi đệ quy tới aroundF. Là một loại chức năng, DFkhông bị thương, vì vậy thực tế f' :: D f xlà không đủ để buộc f' :<-: x :: Z f x.

Sản phẩm bảo toàn tính khác biệt.

data (f :*: g) x = f x :*: g x
instance (Functor f, Functor g) => Functor (f :*: g) where
  fmap h (f :*: g) = fmap h f :*: fmap h g

Để tập trung vào một phần tử trong một cặp, bạn lấy nét ở bên trái và để nguyên bên phải hoặc ngược lại. Quy tắc sản phẩm nổi tiếng của Leibniz tương ứng với một trực giác không gian đơn giản!

instance (Diff1 f, Diff1 g) => Diff1 (f :*: g) where
  type DF (f :*: g) = (DF f :*: g) :+: (f :*: DF g)
  upF (LF (f' :*: g) :<-: x) = upF (f' :<-: x) :*: g
  upF (RF (f :*: g') :<-: x) = f :*: upF (g' :<-: x)

Bây giờ, downFhoạt động tương tự như cách nó đã làm đối với tổng, ngoại trừ việc chúng ta phải sửa ngữ cảnh khóa kéo không chỉ với một thẻ (để hiển thị cách chúng ta đã đi) mà còn với thành phần khác chưa được chạm.

  downF (f :*: g)
    =    fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x) (downF f)
    :*:  fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x) (downF g)

Nhưng aroundFlà một túi cười lớn. Cho dù chúng tôi đang truy cập bên nào, chúng tôi có hai lựa chọn:

  1. Di chuyển aroundFvề phía đó.
  2. Di chuyển upFra khỏi bên đó và downFsang bên kia.

Mỗi trường hợp yêu cầu chúng tôi sử dụng các hoạt động cho cấu trúc con, sau đó sửa các ngữ cảnh.

  aroundF z@(LF (f' :*: g) :<-: (x :: x)) =
    LF (fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x)
          (cxF $ aroundF (f' :<-: x :: ZF f x))
        :*: fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x) (downF g))
    :<-: z
    where f = upF (f' :<-: x)
  aroundF z@(RF (f :*: g') :<-: (x :: x)) =
    RF (fmap (\ (f' :<-: x) -> LF (f' :*: g) :<-: x) (downF f) :*:
        fmap (\ (g' :<-: x) -> RF (f :*: g') :<-: x)
          (cxF $ aroundF (g' :<-: x :: ZF g x)))
    :<-: z
    where g = upF (g' :<-: x)

Phù! Tất cả các đa thức đều có thể phân biệt được và do đó cung cấp cho chúng ta các số viết tắt.

Hừ! Tất cả đều hơi trừu tượng. Vì vậy, tôi đã thêm deriving Showmọi nơi tôi có thể và đưa vào

deriving instance (Show (DF f x), Show x) => Show (ZF f x)

cho phép tương tác sau (thu dọn bằng tay)

> downF (IF 1 :*: IF 2)
IF (LF (KF () :*: IF 2) :<-: 1) :*: IF (RF (IF 1 :*: KF ()) :<-: 2)

> fmap aroundF it
IF  (LF (KF () :*: IF (RF (IF 1 :*: KF ()) :<-: 2)) :<-: (LF (KF () :*: IF 2) :<-: 1))
:*:
IF  (RF (IF (LF (KF () :*: IF 2) :<-: 1) :*: KF ()) :<-: (RF (IF 1 :*: KF ()) :<-: 2))

Bài tập Hãy chứng minh rằng thành phần của các chức năng phân biệt có thể phân biệt được, sử dụng quy tắc dây chuyền .

Ngọt! Chúng ta có thể về nhà bây giờ không? Dĩ nhiên là không. Chúng tôi chưa phân biệt bất kỳ cấu trúc đệ quy nào.

Tạo bộ giải mã đệ quy từ bifunctors

A Bifunctor, như tài liệu hiện có về lập trình chung kiểu dữ liệu (xem công trình của Patrik Jansson và Johan Jeuring, hoặc các ghi chú bài giảng xuất sắc của Jeremy Gibbons) giải thích về độ dài là một phương thức khởi tạo kiểu có hai tham số, tương ứng với hai loại cấu trúc con. Chúng ta sẽ có thể "lập bản đồ" cả hai.

class Bifunctor b where
  bimap :: (x -> x') -> (y -> y') -> b x y -> b x' y'

Chúng ta có thể sử dụng Bifunctors để đưa ra cấu trúc nút của vùng chứa đệ quy. Mỗi nút có subnodesyếu tố . Đây chỉ có thể là hai loại cấu trúc con.

data Mu b y = In (b (Mu b y) y)

Xem? Chúng ta "thắt nút đệ quy" trong bđối số đầu tiên của nó và giữ tham số yở thứ hai. Theo đó, chúng tôi có được một lần cho tất cả

instance Bifunctor b => Functor (Mu b) where
  fmap f (In b) = In (bimap (fmap f) f b)

Để sử dụng điều này, chúng tôi sẽ cần một bộ các Bifunctorphiên bản.

Bộ Bifunctor

Hằng số là hai mặt.

newtype K a x y = K a

instance Bifunctor (K a) where
  bimap f g (K a) = K a

Bạn có thể nói rằng tôi đã viết đoạn này đầu tiên, bởi vì các mã nhận dạng ngắn hơn, nhưng điều đó tốt vì mã dài hơn.

Các biến là hai mặt.

Chúng tôi cần các bifunctors tương ứng với một tham số này hoặc tham số khác, vì vậy tôi đã tạo một kiểu dữ liệu để phân biệt chúng, sau đó xác định một GADT phù hợp.

data Var = X | Y

data V :: Var -> * -> * -> * where
  XX :: x -> V X x y
  YY :: y -> V Y x y

Điều đó tạo ra V X x ymột bản sao của xV Y x ymột bản sao của y. Theo đó

instance Bifunctor (V v) where
  bimap f g (XX x) = XX (f x)
  bimap f g (YY y) = YY (g y)

SumsSản phẩm của bifunctors là bifunctors

data (:++:) f g x y = L (f x y) | R (g x y) deriving Show

instance (Bifunctor b, Bifunctor c) => Bifunctor (b :++: c) where
  bimap f g (L b) = L (bimap f g b)
  bimap f g (R b) = R (bimap f g b)

data (:**:) f g x y = f x y :**: g x y deriving Show

instance (Bifunctor b, Bifunctor c) => Bifunctor (b :**: c) where
  bimap f g (b :**: c) = bimap f g b :**: bimap f g c

Cho đến nay, bảng soạn sẵn, nhưng bây giờ chúng ta có thể xác định những thứ như

List = Mu (K () :++: (V Y :**: V X))

Bin = Mu (V Y :**: (K () :++: (V X :**: V X)))

Nếu bạn muốn sử dụng các kiểu này cho dữ liệu thực tế và không đi theo truyền thống pointilliste của Georges Seurat, hãy sử dụng các từ đồng nghĩa mẫu .

Nhưng khóa kéo là gì? Làm thế nào chúng ta sẽ cho thấy rằng Mu bcó thể phân biệt được? Chúng ta cần chỉ ra rằng bcó thể phân biệt được ở cả hai biến. Kêu vang! Đã đến lúc học về sự khác biệt từng phần.

Các dẫn xuất một phần của phân tử

Bởi vì chúng ta có hai biến số, chúng ta sẽ cần có thể nói về chúng một cách tập thể đôi khi và riêng lẻ vào những thời điểm khác. Chúng ta sẽ cần họ singleton:

data Vary :: Var -> * where
  VX :: Vary X
  VY :: Vary Y

Bây giờ chúng ta có thể nói ý nghĩa của việc Bifunctor có các đạo hàm riêng tại mỗi biến và đưa ra khái niệm về dây kéo tương ứng.

class (Bifunctor b, Bifunctor (D b X), Bifunctor (D b Y)) => Diff2 b where
  type D b (v :: Var) :: * -> * -> *
  up      :: Vary v -> Z b v x y -> b x y
  down    :: b x y -> b (Z b X x y) (Z b Y x y)
  around  :: Vary v -> Z b v x y -> Z b v (Z b X x y) (Z b Y x y)

data Z b v x y = (:<-) {cxZ :: D b v x y, elZ :: V v x y}

Thao Dtác này cần biết biến nào cần nhắm mục tiêu. Khóa kéo tương ứng Z b vcho chúng ta biết biến nào vphải được lấy nét. Khi chúng ta "trang trí với ngữ cảnh", chúng ta phải trang trí x-elements với X-contexts và y-elements với Y-contexts. Nhưng nếu không, đó là một câu chuyện tương tự.

Chúng tôi có hai nhiệm vụ còn lại: thứ nhất, chứng tỏ rằng bộ dụng cụ bifunctor của chúng tôi có thể phân biệt được; thứ hai, để hiển thị rằng Diff2 bcho phép chúng tôi thiết lập Diff1 (Mu b).

Phân biệt bộ Bifunctor

Tôi e rằng điều này là khó hiểu hơn là gây dựng. Vui lòng bỏ qua.

Các hằng số như trước đây.

instance Diff2 (K a) where
  type D (K a) v = K Void
  up _ (K q :<- _) = absurd q
  down (K a) = K a
  around _ (K q :<- _) = absurd q

Nhân dịp này, tuổi thọ quá ngắn để phát triển lý thuyết về loại cấp độ Kronecker-delta, vì vậy tôi chỉ xử lý các biến một cách riêng biệt.

instance Diff2 (V X) where
  type D (V X) X = K ()
  type D (V X) Y = K Void
  up VX (K () :<- XX x)  = XX x
  up VY (K q :<- _)      = absurd q
  down (XX x) = XX (K () :<- XX x)
  around VX z@(K () :<- XX x)  = K () :<- XX z
  around VY (K q :<- _)        = absurd q

instance Diff2 (V Y) where
  type D (V Y) X = K Void
  type D (V Y) Y = K ()
  up VX (K q :<- _)      = absurd q
  up VY (K () :<- YY y)  = YY y
  down (YY y) = YY (K () :<- YY y)
  around VX (K q :<- _)        = absurd q
  around VY z@(K () :<- YY y)  = K () :<- YY z

Đối với các trường hợp cấu trúc, tôi thấy hữu ích khi giới thiệu một trình trợ giúp cho phép tôi xử lý các biến một cách đồng nhất.

vV :: Vary v -> Z b v x y -> V v (Z b X x y) (Z b Y x y)
vV VX z = XX z
vV VY z = YY z

Sau đó, tôi đã xây dựng các tiện ích để hỗ trợ loại "gắn thẻ lại" mà chúng tôi cần downaround. (Tất nhiên, tôi đã thấy những tiện ích nào tôi cần khi làm việc.)

zimap :: (Bifunctor c) => (forall v. Vary v -> D b v x y -> D b' v x y) ->
         c (Z b X x y) (Z b Y x y) -> c (Z b' X x y) (Z b' Y x y)
zimap f = bimap
  (\ (d :<- XX x) -> f VX d :<- XX x)
  (\ (d :<- YY y) -> f VY d :<- YY y)

dzimap :: (Bifunctor (D c X), Bifunctor (D c Y)) =>
         (forall v. Vary v -> D b v x y -> D b' v x y) ->
         Vary v -> Z c v (Z b X x y) (Z b Y x y) -> D c v (Z b' X x y) (Z b' Y x y)
dzimap f VX (d :<- _) = bimap
  (\ (d :<- XX x) -> f VX d :<- XX x)
  (\ (d :<- YY y) -> f VY d :<- YY y)
  d
dzimap f VY (d :<- _) = bimap
  (\ (d :<- XX x) -> f VX d :<- XX x)
  (\ (d :<- YY y) -> f VY d :<- YY y)
  d

Và với lô đó đã sẵn sàng, chúng tôi có thể nghiền nát các chi tiết. Tổng rất dễ dàng.

instance (Diff2 b, Diff2 c) => Diff2 (b :++: c) where
  type D (b :++: c) v = D b v :++: D c v
  up v (L b' :<- vv) = L (up v (b' :<- vv))
  down (L b) = L (zimap (const L) (down b))
  down (R c) = R (zimap (const R) (down c))
  around v z@(L b' :<- vv :: Z (b :++: c) v x y)
    = L (dzimap (const L) v ba) :<- vV v z
    where ba = around v (b' :<- vv :: Z b v x y)
  around v z@(R c' :<- vv :: Z (b :++: c) v x y)
    = R (dzimap (const R) v ca) :<- vV v z
    where ca = around v (c' :<- vv :: Z c v x y)

Sản phẩm là công việc khó khăn, đó là lý do tại sao tôi là một nhà toán học hơn là một kỹ sư.

instance (Diff2 b, Diff2 c) => Diff2 (b :**: c) where
  type D (b :**: c) v = (D b v :**: c) :++: (b :**: D c v)
  up v (L (b' :**: c) :<- vv) = up v (b' :<- vv) :**: c
  up v (R (b :**: c') :<- vv) = b :**: up v (c' :<- vv)
  down (b :**: c) =
    zimap (const (L . (:**: c))) (down b) :**: zimap (const (R . (b :**:))) (down c)
  around v z@(L (b' :**: c) :<- vv :: Z (b :**: c) v x y)
    = L (dzimap (const (L . (:**: c))) v ba :**:
        zimap (const (R . (b :**:))) (down c))
      :<- vV v z where
      b = up v (b' :<- vv :: Z b v x y)
      ba = around v (b' :<- vv :: Z b v x y)
  around v z@(R (b :**: c') :<- vv :: Z (b :**: c) v x y)
    = R (zimap (const (L . (:**: c))) (down b):**:
        dzimap (const (R . (b :**:))) v ca)
      :<- vV v z where
      c = up v (c' :<- vv :: Z c v x y)
      ca = around v (c' :<- vv :: Z c v x y)

Về mặt khái niệm, nó giống như trước đây, nhưng với nhiều quan liêu hơn. Tôi đã tạo ra những thứ này bằng công nghệ lỗ đánh máy trước, sử dụng undefinednhư một sơ khai ở những nơi tôi chưa sẵn sàng làm việc và đưa ra một lỗi loại cố ý ở một nơi (tại bất kỳ thời điểm nào) mà tôi muốn một gợi ý hữu ích từ người đánh máy . Bạn cũng có thể có trải nghiệm đánh máy như trò chơi điện tử, ngay cả trong Haskell.

Khóa kéo phụ cho thùng chứa đệ quy

Đạo hàm riêng của bliên quan đến việc Xcho chúng ta biết làm thế nào để tìm một nút con một bước bên trong một nút, vì vậy chúng ta có khái niệm thông thường về dây kéo.

data MuZpr b y = MuZpr
  {  aboveMu  :: [D b X (Mu b y) y]
  ,  hereMu   :: Mu b y
  }

Chúng ta có thể phóng to đến tận gốc bằng cách cắm nhiều lần vào Xcác vị trí.

muUp :: Diff2 b => MuZpr b y -> Mu b y
muUp (MuZpr {aboveMu = [], hereMu = t}) = t
muUp (MuZpr {aboveMu = (dX : dXs), hereMu = t}) =
  muUp (MuZpr {aboveMu = dXs, hereMu = In (up VX (dX :<- XX t))})

Nhưng chúng ta cần phần tử -zippers.

Khóa kéo phần tử cho các điểm cố định của bộ phân đôi

Mỗi phần tử ở đâu đó bên trong một nút. Nút đó nằm dưới một chồng- Xdẫn xuất. Nhưng vị trí của phần tử trong nút đó được cho bởi một Yđạo hàm. Chúng tôi nhận được

data MuCx b y = MuCx
  {  aboveY  :: [D b X (Mu b y) y]
  ,  belowY  :: D b Y (Mu b y) y
  }

instance Diff2 b => Functor (MuCx b) where
  fmap f (MuCx { aboveY = dXs, belowY = dY }) = MuCx
    {  aboveY  = map (bimap (fmap f) f) dXs
    ,  belowY  = bimap (fmap f) f dY
    }

Mạnh dạn, tôi khẳng định

instance Diff2 b => Diff1 (Mu b) where
  type DF (Mu b) = MuCx b

nhưng trước khi tôi phát triển các hoạt động, tôi sẽ cần một số bit và mảnh.

Tôi có thể trao đổi dữ liệu giữa dây khóa kéo và dây khóa kéo bifunctor như sau:

zAboveY :: ZF (Mu b) y -> [D b X (Mu b y) y]  -- the stack of `X`-derivatives above me
zAboveY (d :<-: y) = aboveY d

zZipY :: ZF (Mu b) y -> Z b Y (Mu b y) y      -- the `Y`-zipper where I am
zZipY (d :<-: y) = belowY d :<- YY y

Điều đó đủ để tôi định nghĩa:

  upF z  = muUp (MuZpr {aboveMu = zAboveY z, hereMu = In (up VY (zZipY z))})

Đó là, trước tiên chúng ta đi lên bằng cách ráp lại nút nơi có phần tử, biến một phần tử-dây kéo thành một khóa kéo phụ, sau đó thu phóng hết cỡ, như trên.

Tiếp theo, tôi nói

  downF  = yOnDown []

để đi xuống bắt đầu với ngăn xếp trống và xác định hàm trợ giúp đi downlặp lại từ bên dưới bất kỳ ngăn xếp nào:

yOnDown :: Diff2 b => [D b X (Mu b y) y] -> Mu b y -> Mu b (ZF (Mu b) y)
yOnDown dXs (In b) = In (contextualize dXs (down b))

Bây giờ, down bchỉ đưa chúng ta vào bên trong nút. Các khóa kéo chúng ta cần cũng phải mang ngữ cảnh của nút. Đó là những gì contextualiselàm:

contextualize :: (Bifunctor c, Diff2 b) =>
  [D b X (Mu b y) y] ->
  c (Z b X (Mu b y) y) (Z b Y (Mu b y) y) ->
  c (Mu b (ZF (Mu b) y)) (ZF (Mu b) y)
contextualize dXs = bimap
  (\ (dX :<- XX t) -> yOnDown (dX : dXs) t)
  (\ (dY :<- YY y) -> MuCx {aboveY = dXs, belowY = dY} :<-: y)

Đối với mọi Y-position, chúng ta phải cung cấp một phần tử-zipper, vì vậy tốt là chúng ta biết toàn bộ ngữ cảnh dXstrở về gốc, cũng như bối cảnh dYmô tả cách phần tử nằm trong nút của nó. Đối với mỗi- Xvị trí, có một cây con khác để khám phá, vì vậy chúng tôi phát triển ngăn xếp và tiếp tục!

Điều đó chỉ còn lại việc kinh doanh chuyển trọng tâm. Chúng ta có thể ở yên, hoặc đi xuống từ vị trí của chúng ta, hoặc đi lên, hoặc đi lên rồi đi xuống một con đường khác. Đây rồi.

  aroundF z@(MuCx {aboveY = dXs, belowY = dY} :<-: _) = MuCx
    {  aboveY = yOnUp dXs (In (up VY (zZipY z)))
    ,  belowY = contextualize dXs (cxZ $ around VY (zZipY z))
    }  :<-: z

Như mọi khi, phần tử hiện có được thay thế bằng toàn bộ dây kéo của nó. Về belowYphần, chúng tôi xem xét nơi khác mà chúng tôi có thể đi trong nút hiện có: chúng tôi sẽ tìm thấy phần tử thay thế -positions Yhoặc- Xsubnode khác để khám phá, vì vậy chúng tôi tìm contextualisechúng. Về aboveYphần mình, chúng ta phải làm việc theo cách của chúng ta để sao lưu chồng- Xdẫn xuất sau khi tập hợp lại nút mà chúng ta đã truy cập.

yOnUp :: Diff2 b => [D b X (Mu b y) y] -> Mu b y ->
         [D b X (Mu b (ZF (Mu b) y)) (ZF (Mu b) y)]
yOnUp [] t = []
yOnUp (dX : dXs) (t :: Mu b y)
  =  contextualize dXs (cxZ $ around VX (dX :<- XX t))
  :  yOnUp dXs (In (up VX (dX :<- XX t)))

Ở mỗi bước của con đường, chúng ta có thể rẽ sang một nơi khác around, hoặc tiếp tục đi lên.

Và đó là nó! Tôi chưa đưa ra bằng chứng chính thức về luật, nhưng đối với tôi, tôi thấy như thể các thao tác cẩn thận duy trì ngữ cảnh một cách chính xác khi chúng thu thập cấu trúc.

Chúng ta đã học được gì?

Khả năng khác biệt tạo ra khái niệm về sự vật trong ngữ cảnh của nó, tạo ra một cấu trúc hài hòa nơi extractcung cấp cho bạn sự vật và duplicatekhám phá bối cảnh tìm kiếm những thứ khác để bối cảnh hóa. Nếu chúng ta có cấu trúc vi phân thích hợp cho các nút, chúng ta có thể phát triển cấu trúc vi phân cho toàn bộ cây.

Ồ, và việc đối xử với từng trình tạo kiểu riêng biệt là một điều kinh khủng trắng trợn. Cách tốt hơn là làm việc với các chức năng giữa các nhóm được lập chỉ mục

f :: (i -> *) -> (o -> *)

nơi chúng tôi tạo ra ocác loại cấu trúc ikhác nhau lưu trữ các loại phần tử khác nhau. Chúng được đóng lại dưới sự xây dựng của Jacobian

J f :: (i -> *) -> ((o, i) -> *)

trong đó mỗi (o, i)-cấu trúc kết quả là một đạo hàm riêng, cho bạn biết cách tạo một ilỗ trống trong một- ocấu trúc. Nhưng đó là niềm vui được đánh máy phụ thuộc vào một thời gian khác.


2
Sử dụng "kiểm tra kiểu như một trò chơi điện tử", hay đúng hơn, chỉ suy luận về các loại, tôi đã vượt qua Comonadcấp độ, nhưng chỉ có thể đi đến một kết thúc thay thế. Chơi trò chơi, tôi gặp phải một mức độ thú vị và khó khăn. Người đánh máy cho biết loại lỗ là a -> a(đối với một số loại dài lớn a), nhưng việc lấp lỗ bằng idkhông có tác dụng. Vấn đề là ở chỗ a ~ D t ~ D r, và tôi thực sự cần một hàm D r -> D tvà cần cung cấp cho người đánh máy bằng chứng về D r ~ D t.
Cirdec

3
do đó, hãy cẩn thận sử dụng ScopedTypeVariables, cho những khoảnh khắc khi ghc nói (bằng giọng của SPJ) "Không không không, tôi muốn đoán !" nhưng đoán khó quá.
pigworker

12
Câu trả lời ngắn gọn dường như là Diff cũng cần aroundchữ ký của mình. Câu trả lời dài, tốt, luôn mở rộng tầm mắt một cách tuyệt vời. Cảm ơn rất nhiều vì đã dành một phút để viết điều này!
J. Abrahamson

1
Các phần đi vào downaroundgiống nhau. Có vẻ như chúng ta sẽ có thể chỉ định cả hai cho ví dụ như các sản phẩm bằng một cái gì đó như descend f (a :*: b) = pure (:*:) <*> f (InL . (:*: b)) a <*> f (InR . (a :*:)) bnơi descendcó một loại dọc theo dòng Applicative (m t) => (forall f g. (Diff f, Diff g) => (D f a -> D g a) -> f a -> m g (f a)) -> t a -> m t (t a).
Cirdec

1
aroundcó thể được viết hoàn toàn dưới dạng down, upvà dẫn xuất thứ hai, sử dụng lại mã từ updownkhông yêu cầu thêm một phần trừu tượng Applicativeđể nắm bắt nó.
Cirdec

12

Các Comonadví dụ cho dây kéo là không

instance (Diff t, Diff (D t)) => Comonad (Zipper t) where
    extract = here
    duplicate = fmap outOf . inTo

ở đâu outOfinTođến từ Diffví dụ cho Zipper tchính nó. Trường hợp trên vi phạm Comonadpháp luật fmap extract . duplicate == id. Thay vào đó, nó hoạt động như sau:

fmap extract . duplicate == \z -> fmap (const (here z)) z

Diff (Dây kéo t)

Các Diffví dụ cho Zipperđược cung cấp bằng cách xác định chúng như các sản phẩm và tái sử dụng mã cho sản phẩm (dưới đây).

-- Zippers are themselves products
toZipper :: (D t :*: Identity) a -> Zipper t a
toZipper (d :*: (Identity h)) = Zipper d h

fromZipper :: Zipper t a -> (D t :*: Identity) a
fromZipper (Zipper d h) = (d :*: (Identity h))

Đưa ra một đẳng cấu giữa các kiểu dữ liệu và một đẳng cấu giữa các dẫn xuất của chúng, chúng ta có thể sử dụng lại kiểu này inTooutOfcho kiểu kia.

inToFor' :: (Diff r) =>
            (forall a.   r a ->   t a) ->
            (forall a.   t a ->   r a) ->
            (forall a. D r a -> D t a) ->
            (forall a. D t a -> D r a) ->
            t a -> t (Zipper t a)
inToFor' to from toD fromD = to . fmap (onDiff toD) . inTo . from

outOfFor' :: (Diff r) =>
            (forall a.   r a ->   t a) ->
            (forall a.   t a ->   r a) ->
            (forall a. D r a -> D t a) ->
            (forall a. D t a -> D r a) ->
            Zipper t a -> t a
outOfFor' to from toD fromD = to . outOf . onDiff fromD

Đối với các kiểu chỉ là Kiểu mới cho một phiên bản hiện có Diff, các dẫn xuất của chúng là cùng một kiểu. Nếu chúng ta nói với trình kiểm tra kiểu về sự bình đẳng kiểu đó D r ~ D t, chúng ta có thể tận dụng điều đó thay vì cung cấp một đẳng cấu cho các dẫn xuất.

inToFor :: (Diff r, D r ~ D t) =>
           (forall a. r a -> t a) ->
           (forall a. t a -> r a) ->
           t a -> t (Zipper t a)
inToFor to from = inToFor' to from id id

outOfFor :: (Diff r, D r ~ D t) =>
            (forall a. r a -> t a) ->
            (forall a. t a -> r a) ->
            Zipper t a -> t a
outOfFor to from = outOfFor' to from id id

Được trang bị những công cụ này, chúng tôi có thể sử dụng lại Diffphiên bản cho các sản phẩm để triển khaiDiff (Zipper t)

-- This requires undecidable instances, due to the need to take D (D t)
instance (Diff t, Diff (D t)) => Diff (Zipper t) where
    type D (Zipper t) = D ((D t) :*: Identity)
    -- inTo :: t        a -> t        (Zipper  t         a)
    -- inTo :: Zipper t a -> Zipper t (Zipper (Zipper t) a)
    inTo = inToFor toZipper fromZipper
    -- outOf :: Zipper  t         a -> t        a
    -- outOf :: Zipper (Zipper t) a -> Zipper t a
    outOf = outOfFor toZipper fromZipper

bản mẫu

Để thực sự sử dụng mã được trình bày ở đây, chúng tôi cần một số tiện ích mở rộng ngôn ngữ, nhập khẩu và trình bày lại vấn đề được đề xuất.

{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE RankNTypes #-}

import Control.Monad.Identity
import Data.Proxy
import Control.Comonad

data Zipper t a = Zipper { diff :: D t a, here :: a }

onDiff :: (D t a -> D u a) -> Zipper t a -> Zipper u a
onDiff f (Zipper d a) = Zipper (f d) a

deriving instance Diff t => Functor (Zipper t)
deriving instance (Eq (D t a), Eq a) => Eq (Zipper t a)
deriving instance (Show (D t a), Show a) => Show (Zipper t a)

class (Functor t, Functor (D t)) => Diff t where
  type D t :: * -> *
  inTo  :: t a -> t (Zipper t a)
  outOf :: Zipper t a -> t a

Sản phẩm, Tổng và Hằng số

Các Diff (Zipper t)ví dụ dựa vào hiện thực của Diffsản phẩm :*:, tiền :+:, hằng Identity, và zero Proxy.

data (:+:) a b x = InL (a x) | InR (b x)
    deriving (Eq, Show)
data (:*:) a b x = a x :*: b x
    deriving (Eq, Show)

infixl 7 :*:
infixl 6 :+:

deriving instance (Functor a, Functor b) => Functor (a :*: b)

instance (Functor a, Functor b) => Functor (a :+: b) where
    fmap f (InL a) = InL . fmap f $ a
    fmap f (InR b) = InR . fmap f $ b


instance (Diff a, Diff b) => Diff (a :*: b) where
    type D (a :*: b) = D a :*: b :+: a :*: D b
    inTo (a :*: b) = 
        (fmap (onDiff (InL . (:*: b))) . inTo) a :*:
        (fmap (onDiff (InR . (a :*:))) . inTo) b
    outOf (Zipper (InL (a :*: b)) x) = (:*: b) . outOf . Zipper a $ x
    outOf (Zipper (InR (a :*: b)) x) = (a :*:) . outOf . Zipper b $ x

instance (Diff a, Diff b) => Diff (a :+: b) where
    type D (a :+: b) = D a :+: D b
    inTo (InL a) = InL . fmap (onDiff InL) . inTo $ a
    inTo (InR b) = InR . fmap (onDiff InR) . inTo $ b
    outOf (Zipper (InL a) x) = InL . outOf . Zipper a $ x
    outOf (Zipper (InR a) x) = InR . outOf . Zipper a $ x

instance Diff (Identity) where
    type D (Identity) = Proxy
    inTo = Identity . (Zipper Proxy) . runIdentity
    outOf = Identity . here

instance Diff (Proxy) where
    type D (Proxy) = Proxy
    inTo = const Proxy
    outOf = const Proxy

Ví dụ về thùng rác

Tôi đặt Binví dụ như một phép đẳng cấu cho một tổng các sản phẩm. Chúng ta không chỉ cần đạo hàm của nó mà cả đạo hàm thứ hai của nó nữa

newtype Bin   a = Bin   {unBin   ::      (Bin :*: Identity :*: Bin :+: Identity)  a}
    deriving (Functor, Eq, Show)
newtype DBin  a = DBin  {unDBin  ::    D (Bin :*: Identity :*: Bin :+: Identity)  a}
    deriving (Functor, Eq, Show)
newtype DDBin a = DDBin {unDDBin :: D (D (Bin :*: Identity :*: Bin :+: Identity)) a}
    deriving (Functor, Eq, Show)

instance Diff Bin where
    type D Bin = DBin
    inTo  = inToFor'  Bin unBin DBin unDBin
    outOf = outOfFor' Bin unBin DBin unDBin

instance Diff DBin where
    type D DBin = DDBin
    inTo  = inToFor'  DBin unDBin DDBin unDDBin
    outOf = outOfFor' DBin unDBin DDBin unDDBin

Dữ liệu ví dụ từ câu trả lời trước

aTree :: Bin Int    
aTree =
    (Bin . InL) (
        (Bin . InL) (
            (Bin . InR) (Identity 2)
            :*: (Identity 1) :*:
            (Bin . InR) (Identity 3)
        )
        :*: (Identity 0) :*:
        (Bin . InR) (Identity 4)
    )

Không phải trường hợp Comonad

Các Binví dụ trên cung cấp một phản ví dụ để fmap outOf . inTođược thực hiện đúng duplicatecho Zipper t. Đặc biệt, nó cung cấp một ví dụ phản đối fmap extract . duplicate = idluật:

fmap ( \z -> (fmap extract . duplicate) z == z) . inTo $ aTree

Điều nào được đánh giá là (lưu ý rằng nó có đầy đủ các Falses ở khắp mọi nơi, bất kỳ điều nào Falsecũng đủ để bác bỏ luật)

Bin {unBin = InL ((Bin {unBin = InL ((Bin {unBin = InR (Identity False)} :*: Identity False) :*: Bin {unBin = InR (Identity False)})} :*: Identity False) :*: Bin {unBin = InR (Identity False)})}

inTo aTreelà cây có cùng cấu tạo aTreenhưng chỗ nào có giá trị thì ở đó có dây kéo có giá trị, phần còn lại của cây còn nguyên giá trị ban đầu. fmap (fmap extract . duplicate) . inTo $ aTreecũng là một cây có cùng cấu trúc như aTree, nhưng mỗi khi có một giá trị thì thay vào đó sẽ có một dây kéo với giá trị đó và phần còn lại của cây với tất cả các giá trị được thay thế bằng cùng một giá trị đó . Nói cách khác:

fmap extract . duplicate == \z -> fmap (const (here z)) z

Các thử nghiệm-suite đầy đủ cho cả ba Comonadluật, extract . duplicate == id, fmap extract . duplicate == id, và duplicate . duplicate == fmap duplicate . duplicate

main = do
    putStrLn "fmap (\\z -> (extract . duplicate) z == z) . inTo $ aTree"
    print   . fmap ( \z -> (extract . duplicate) z == z) . inTo $ aTree    
    putStrLn ""
    putStrLn  "fmap (\\z -> (fmap extract . duplicate) z == z) . inTo $ aTree"
    print    . fmap ( \z -> (fmap extract . duplicate) z == z) . inTo $ aTree    
    putStrLn ""
    putStrLn "fmap (\\z -> (duplicate . duplicate) z) == (fmap duplicate . duplicate) z) . inTo $ aTree"
    print   . fmap ( \z -> (duplicate . duplicate) z == (fmap duplicate . duplicate) z) . inTo $ aTree

1
updowntừ blog của Conal cũng giống như intooutof.
J. Abrahamson

Tôi có thể thấy rằng @pigworker đã cố gắng đi theo con đường mà tôi đang cố gắng đi xuống một năm trước. stackoverflow.com/questions/14133121/…
Cirdec,

8

Đưa ra một Difflớp có thể phân biệt vô hạn :

class (Functor t, Functor (D t)) => Diff t where
    type D t :: * -> *
    up :: Zipper t a -> t a
    down :: t a -> t (Zipper t a)  
    -- Require that types be infinitely differentiable
    ddiff :: p t -> Dict (Diff (D t))

aroundcó thể được viết dưới dạng updownvề nguồn gốc Zippercủa diff's, về cơ bản là

around z@(Zipper d h) = Zipper ctx z
    where
        ctx = fmap (\z' -> Zipper (up z') (here z')) (down d)

Các Zipper t abao gồm một D t avà một a. Chúng tôi đi downvề D t a, nhận được một D t (Zipper (D t) a)với một dây kéo trong mỗi lỗ. Những khóa kéo đó bao gồm một D (D t) avà một atrong lỗ. Chúng tôi đi uptừng người trong số họ, lấy một D t avà cắt nó với acái đã có trong lỗ. A D t avà an amake a Zipper t a, cho chúng ta a D t (Zipper t a), đó là ngữ cảnh cần thiết cho a Zipper t (Zipper t a).

Các Comonadví dụ là sau đó chỉ cần

instance Diff t => Comonad (Zipper t) where
    extract   = here
    duplicate = around

Việc nắm bắt từ Diffđiển của phái sinh yêu cầu một số hệ thống ống nước bổ sung, điều này có thể được thực hiện với Data.Constraint hoặc theo phương pháp được trình bày trong một câu trả lời liên quan

around :: Diff t => Zipper t a -> Zipper t (Zipper t a)
around z = Zipper (withDict d' (fmap (\z' -> Zipper (up z') (here z')) (down (diff z)))) z
    where
        d' = ddiff . p' $ z
        p' :: Zipper t x -> Proxy t
        p' = const Proxy 

Đánh lừa xung quanh với điều này một chút, nó có vẻ hoạt động tốt: gist.github.com/tel/fae4f90f47a9eda0373b . Tôi rất muốn xem liệu mình có thể điều khiển dây kéo tùy chỉnh xuống đất và sau đó sử dụng nó để có được tự động hay aroundkhông.
J. Abrahamson

2
Cách đầu tiên aroundcũng đánh máy có around :: (Diff t, Diff (D t)) => Zipper t a -> Zipper t (Zipper t a)và không có ddiffphương pháp, và tương tự như vậy Comonad, vì vậy khả năng phân biệt hai lần dường như là đủ.
Ørjan Johansen
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.