Đơn nguyên được lập chỉ mục là gì?


98

Đơn nguyên được lập chỉ mục là gì và động lực cho đơn nguyên này là gì?

Tôi đã đọc rằng nó giúp theo dõi các tác dụng phụ. Nhưng chữ ký kiểu và tài liệu không dẫn tôi đến bất cứ đâu.

Ví dụ nào về cách nó có thể giúp theo dõi các tác dụng phụ (hoặc bất kỳ ví dụ hợp lệ nào khác)?

Câu trả lời:


123

Từ trước đến nay, thuật ngữ mọi người sử dụng không hoàn toàn nhất quán. Có rất nhiều khái niệm lấy cảm hứng từ monads-nhưng-nói-đúng-không-hoàn-toàn. Thuật ngữ "đơn nguyên được lập chỉ mục" là một trong số (bao gồm "đơn nguyên" và "đơn nguyên được tham số hóa" (tên của Atkey cho chúng)) các thuật ngữ được sử dụng để mô tả một khái niệm như vậy. (Một khái niệm khác như vậy, nếu bạn quan tâm, là "đơn nguyên hiệu ứng tham số" của Katsumata, được lập chỉ mục bởi một monoid, trong đó lợi nhuận được lập chỉ mục trung lập và ràng buộc được tích lũy trong chỉ mục của nó.)

Trước hết, hãy kiểm tra các loại.

IxMonad (m :: state -> state -> * -> *)

Đó là, loại "tính toán" (hoặc "hành động", nếu bạn thích, nhưng tôi sẽ gắn bó với "tính toán"), trông giống như

m before after value

ở đâu before, after :: statevalue :: *. Ý tưởng là nắm bắt các phương tiện để tương tác một cách an toàn với một hệ thống bên ngoài có một số khái niệm về trạng thái có thể đoán trước được . Kiểu của một máy tính cho bạn biết trạng thái beforemà nó phải chạy, trạng thái sẽ afterchạy và (giống như với các đơn nguyên thông thường *) mà máy valuetính tạo ra kiểu gì.

Các bit và mảnh thông thường - *tương tự như một đơn nguyên và state- tương tự như chơi domino.

ireturn  ::  a -> m i i a    -- returning a pure value preserves state
ibind    ::  m i j a ->      -- we can go from i to j and get an a, thence
             (a -> m j k b)  -- we can go from j to k and get a b, therefore
             -> m i k b      -- we can indeed go from i to k and get a b

Khái niệm "mũi tên Kleisli" (hàm mang lại khả năng tính toán) do đó được tạo ra là

a -> m i j b   -- values a in, b out; state transition i to j

và chúng tôi nhận được một bố cục

icomp :: IxMonad m => (b -> m j k c) -> (a -> m i j b) -> a -> m i k c
icomp f g = \ a -> ibind (g a) f

và, luật pháp đảm bảo chính xác điều đó ireturnicompcung cấp cho chúng tôi một danh mục

      ireturn `icomp` g = g
      f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)

hoặc, trong C / Java giả mạo hài hước / bất cứ điều gì,

      g(); skip = g()
      skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}

Quan tâm làm gì? Để mô hình hóa "quy tắc" của sự tương tác. Ví dụ: bạn không thể đẩy đĩa dvd ra nếu không có một đĩa nào trong ổ đĩa và bạn không thể đưa một đĩa dvd vào ổ đĩa nếu đã có một đĩa đệm trong đó. Vì thế

data DVDDrive :: Bool -> Bool -> * -> * where  -- Bool is "drive full?"
  DReturn :: a -> DVDDrive i i a
  DInsert :: DVD ->                   -- you have a DVD
             DVDDrive True k a ->     -- you know how to continue full
             DVDDrive False k a       -- so you can insert from empty
  DEject  :: (DVD ->                  -- once you receive a DVD
              DVDDrive False k a) ->  -- you know how to continue empty
             DVDDrive True k a        -- so you can eject when full

instance IxMonad DVDDrive where  -- put these methods where they need to go
  ireturn = DReturn              -- so this goes somewhere else
  ibind (DReturn a)     k  = k a
  ibind (DInsert dvd j) k  = DInsert dvd (ibind j k)
  ibind (DEject j)      k  = DEject j $ \ dvd -> ibind (j dvd) k

Với điều này tại chỗ, chúng ta có thể xác định các lệnh "nguyên thủy"

dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()

dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd

từ đó những người khác được lắp ráp với ireturnibind. Bây giờ, tôi có thể viết (mượn- dochú thích)

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'

nhưng không phải là không thể

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject      -- ouch!

Ngoài ra, người ta có thể xác định trực tiếp các lệnh nguyên thủy của một người

data DVDCommand :: Bool -> Bool -> * -> * where
  InsertC  :: DVD -> DVDCommand False True ()
  EjectC   :: DVDCommand True False DVD

và sau đó khởi tạo mẫu chung

data CommandIxMonad :: (state -> state -> * -> *) ->
                        state -> state -> * -> * where
  CReturn  :: a -> CommandIxMonad c i i a
  (:?)     :: c i j a -> (a -> CommandIxMonad c j k b) ->
                CommandIxMonad c i k b

instance IxMonad (CommandIxMonad c) where
  ireturn = CReturn
  ibind (CReturn a) k  = k a
  ibind (c :? j)    k  = c :? \ a -> ibind (j a) k

Trên thực tế, chúng tôi đã nói các mũi tên Kleisli nguyên thủy là gì ("domino" là gì), sau đó xây dựng một khái niệm phù hợp về "chuỗi tính toán" cho chúng.

Lưu ý rằng đối với mọi đơn nguyên được lập chỉ mục m, "đường chéo không thay đổi" m i ilà một đơn nguyên, nhưng nói chung, m i jkhông phải. Hơn nữa, các giá trị không được lập chỉ mục nhưng các tính toán được lập chỉ mục, vì vậy một đơn nguyên được lập chỉ mục không chỉ là ý tưởng thông thường của đơn nguyên được khởi tạo cho một số danh mục khác.

Bây giờ, hãy nhìn lại loại mũi tên Kleisli

a -> m i j b

Chúng tôi biết rằng chúng tôi phải ở trạng thái iđể bắt đầu và chúng tôi dự đoán rằng bất kỳ sự tiếp tục nào cũng sẽ bắt đầu từ trạng thái j. Chúng tôi biết rất nhiều về hệ thống này! Đây không phải là một hoạt động mạo hiểm! Khi chúng tôi đặt dvd vào ổ đĩa, nó sẽ chạy vào! Ổ đĩa dvd không nhận được bất kỳ tiếng nói nào về trạng thái sau mỗi lệnh.

Nhưng nói chung điều đó không đúng, khi tương tác với thế giới. Đôi khi bạn có thể cần phải cho đi một số quyền kiểm soát và để thế giới làm những gì nó thích. Ví dụ: nếu bạn là một máy chủ, bạn có thể cung cấp cho khách hàng của mình lựa chọn và trạng thái phiên của bạn sẽ phụ thuộc vào những gì họ chọn. Thao tác "lựa chọn ưu đãi" của máy chủ không xác định trạng thái kết quả, nhưng dù sao thì máy chủ vẫn có thể tiếp tục. Nó không phải là một "lệnh nguyên thủy" theo nghĩa trên, vì vậy các mona được lập chỉ mục không phải là một công cụ tốt để mô hình hóa kịch bản không thể đoán trước .

Công cụ nào tốt hơn?

type f :-> g = forall state. f state -> g state

class MonadIx (m :: (state -> *) -> (state -> *)) where
  returnIx    :: x :-> m x
  flipBindIx  :: (a :-> m b) -> (m a :-> m b)  -- tidier than bindIx

Bánh quy đáng sợ? Không thực sự, vì hai lý do. Một, nó trông giống như một đơn nguyên hơn, bởi vì nó một đơn nguyên, nhưng hơn (state -> *)là hơn *. Hai, nếu bạn nhìn vào loại mũi tên Kleisli,

a :-> m b   =   forall state. a state -> m b state

bạn sẽ có được kiểu tính toán với điều kiện trước a và điều kiện sau b, giống như trong Good Old Hoare Logic. Các xác nhận trong lôgic chương trình đã mất hơn nửa thế kỷ để vượt qua thư từ Curry-Howard và trở thành kiểu Haskell. Loại returnIxcâu nói "bạn có thể đạt được bất kỳ điều kiện hậu cần nào, chỉ cần không làm gì cả", đó là quy tắc Hoare Logic cho "bỏ qua". Thành phần tương ứng là quy tắc Hoare Logic cho ";".

Hãy kết thúc bằng cách xem xét loại bindIx, đưa tất cả các bộ định lượng vào.

bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i

Các foralls này có cực tính trái ngược nhau. Chúng tôi chọn trạng thái ban đầu ivà tính toán có thể bắt đầu tại i, với điều kiện sau a. Thế giới chọn bất kỳ trạng thái trung gian nào jmà nó thích, nhưng nó phải cung cấp cho chúng ta bằng chứng rằng điều kiện hậu btồn tại, và từ bất kỳ trạng thái nào như vậy, chúng ta có thể tiếp tục bgiữ vững. Vì vậy, theo trình tự, chúng ta có thể đạt được điều kiện btừ trạng thái i. Bằng cách giải phóng sự kìm kẹp của chúng ta đối với các trạng thái "sau", chúng ta có thể lập mô hình các phép tính không thể đoán trước .

Cả hai IxMonadMonadIxđều hữu ích. Cả hai mô hình tính hợp lệ của các tính toán tương tác liên quan đến trạng thái thay đổi, có thể dự đoán và không thể đoán trước, tương ứng. Khả năng dự đoán có giá trị khi bạn có thể có được nó, nhưng không thể đoán trước đôi khi là một thực tế của cuộc sống. Hy vọng rằng, câu trả lời này cung cấp một số dấu hiệu về các đơn nguyên được lập chỉ mục là gì, dự đoán cả khi chúng bắt đầu hữu ích và khi nào chúng dừng lại.


1
Làm cách nào bạn có thể truyền True/ Falsegiá trị dưới dạng đối số kiểu DVDDrive? Đó là một số tiện ích mở rộng, hay là các boolean thực sự nhập vào đây?
Bergi

8
@Bergi Các boolean đã được "nâng cấp" để tồn tại ở mức kiểu. Điều này có thể thực hiện được trong Haskell bằng cách sử dụng DataKindstiện ích mở rộng và bằng các ngôn ngữ được đánh máy phụ thuộc ... tốt, đó là toàn bộ.
J. Abrahamson

Bạn có thể mở rộng một chút MonadIx, có lẽ với các ví dụ? Nó tốt hơn trên cơ sở lý thuyết, hay tốt hơn cho ứng dụng thực tế?
Christian Conkle

2
@ChristianConkle Tôi nhận thấy điều đó không hữu ích lắm. Nhưng bạn nêu ra điều gì thực sự là một câu hỏi hoàn toàn khác. Ở địa phương khi tôi nói MonadIx là "tốt hơn", ý tôi là trong bối cảnh mô hình hóa các tương tác với một môi trường không thể đoán trước. Giống như nếu ổ đĩa dvd của bạn được phép lấy ra các đĩa dvd mà nó không thích khi bạn cố gắng chèn chúng. Một số tình huống thực tế được ứng xử tồi tệ như vậy. Những người khác có khả năng dự đoán cao hơn (có nghĩa là bạn có thể nói rằng bất kỳ sự tiếp diễn nào bắt đầu ở trạng thái nào, chứ không phải là các hoạt động không thất bại), trong trường hợp đó IxMonad dễ làm việc hơn.
pigworker

1
Khi bạn "mượn" ký hiệu trong câu trả lời, có thể hữu ích khi nói rằng nó thực sự là cú pháp hợp lệ với RebindableSyntaxphần mở rộng. Đề cập đến các tiện ích mở rộng bắt buộc khác sẽ rất tuyệt, như đã nói ở trênDataKinds
gigabyte

46

Có ít nhất ba cách để xác định một đơn nguyên được lập chỉ mục mà tôi biết.

Tôi sẽ gọi các tùy chọn này là đơn nguyên được lập chỉ mục à la X , trong đó X nằm trên các nhà khoa học máy tính Bob Atkey, Conor McBride và Dominic Orchard, vì đó là cách tôi có xu hướng nghĩ về chúng. Các phần của những công trình này có lịch sử lâu đời hơn lừng lẫy hơn nhiều và cách diễn giải đẹp hơn thông qua lý thuyết phạm trù, nhưng lần đầu tiên tôi biết về chúng gắn liền với những cái tên này và tôi đang cố gắng giữ cho câu trả lời này không quá bí truyền.

Atkey

Phong cách của đơn nguyên được lập chỉ mục của Bob Atkey là làm việc với 2 tham số phụ để xử lý chỉ mục của đơn nguyên.

Với điều đó, bạn sẽ có được các định nghĩa mà mọi người đã xoay quanh các câu trả lời khác:

class IMonad m where
  ireturn  ::  a -> m i i a
  ibind    ::  m i j a -> (a -> m j k b) -> m i k b

Chúng ta cũng có thể định nghĩa các comonads à la Atkey được lập chỉ mục. Tôi thực sự nhận được rất nhiều dặm từ những người trong lenscơ sở mã .

McBride

Hình thức tiếp theo của đơn nguyên được lập chỉ mục là định nghĩa của Conor McBride từ bài báo của ông "Kleisli Arrows of Outrageous Fortune" . Thay vào đó, anh ta sử dụng một tham số duy nhất cho chỉ mục. Điều này làm cho định nghĩa đơn nguyên được lập chỉ mục có một hình dạng khá thông minh.

Nếu chúng ta xác định một phép biến đổi tự nhiên bằng cách sử dụng tham số như sau

type a ~> b = forall i. a i -> b i 

thì chúng ta có thể viết ra định nghĩa của McBride là

class IMonad m where
  ireturn :: a ~> m a
  ibind :: (a ~> m b) -> (m a ~> m b)

Điều này cảm thấy khá khác so với Atkey, nhưng nó giống như một Đơn nguyên bình thường hơn, thay vì xây dựng đơn nguyên (m :: * -> *), chúng tôi xây dựng nó (m :: (k -> *) -> (k -> *).

Điều thú vị là bạn thực sự có thể khôi phục kiểu đơn nguyên được lập chỉ mục của Atkey từ McBride bằng cách sử dụng một kiểu dữ liệu thông minh, mà McBride theo phong cách không thể bắt chước của mình chọn để nói rằng bạn nên đọc là "tại chìa khóa".

data (:=) :: a i j where
   V :: a -> (a := i) i

Bây giờ bạn có thể tìm ra điều đó

ireturn :: IMonad m => (a := j) ~> m (a := j)

mở rộng thành

ireturn :: IMonad m => (a := j) i -> m (a := j) i

chỉ có thể được gọi khi j = i, và sau đó đọc kỹ ibindcó thể đưa bạn trở lại giống như Atkey ibind. Bạn cần chuyển xung quanh các cấu trúc dữ liệu (: =) này, nhưng chúng phục hồi sức mạnh của bản trình bày Atkey.

Mặt khác, bản trình bày Atkey không đủ mạnh để khôi phục tất cả các hoạt động sử dụng phiên bản của McBride. Quyền lực đã đạt được một cách nghiêm ngặt.

Một điều tốt đẹp khác là đơn nguyên được lập chỉ mục của McBride rõ ràng là một đơn nguyên, nó chỉ là một đơn nguyên trên một danh mục chức năng khác. Nó hoạt động trên endofunctors trên danh mục functors từ (k -> *)đến (k -> *)hơn là danh mục functors từ *đến *.

Một tập thể dục vui vẻ là tìm hiểu làm thế nào để làm McBride để Atkey chuyển đổi cho lập chỉ mục comonads . Cá nhân tôi sử dụng kiểu dữ liệu 'At' cho cấu trúc "at key" trong bài báo của McBride. Tôi thực sự đã đến gặp Bob Atkey tại ICFP 2013 và đề cập rằng tôi muốn biến anh ấy từ trong ra ngoài để biến anh ấy thành "Áo khoác". Anh ta có vẻ bị xáo trộn rõ ràng. Dòng chơi tốt hơn trong đầu tôi. =)

Vườn cây ăn quả

Cuối cùng, người khiếu nại thứ ba ít được đề cập đến với tên gọi "đơn nguyên được lập chỉ mục" là do Dominic Orchard, nơi anh ta thay vào đó sử dụng một đơn nguyên cấp loại để phá vỡ các chỉ số. Thay vì đi qua các chi tiết của việc xây dựng, tôi sẽ chỉ liên kết đến bài nói chuyện này:

https://github.com/dorchard/effect-monad/blob/master/docs/ixmonad-fita14.pdf


1
Tôi có đúng không khi đơn nguyên của Orchard tương đương với đơn nguyên của Atkey, vì chúng ta có thể đi từ đơn nguyên trước đến đơn nguyên sau bằng cách lấy đơn nguyên tố nội sinh và đi ngược lại bằng đơn nguyên mã hóa CPS gắn vào trong quá trình chuyển đổi trạng thái?
András Kovács

Điều đó nghe có vẻ hợp lý đối với tôi.
Edward KMETT

Điều đó nói rằng, dựa trên những điều anh ấy nói với tôi tại ICFP 2013, tôi tin rằng Orchard dự định các gia đình loại của anh ấy hoạt động giống như một đơn vị thực sự chứ không phải là một danh mục tùy ý mà một số mũi tên không thể kết nối, vì vậy có thể có nhiều điều hơn trong câu chuyện hơn thế nữa, vì cấu trúc của Atkey cho phép bạn dễ dàng hạn chế một số hành động của Kleisli kết nối với những người khác - theo nhiều cách, đó là điểm chính của nó và phiên bản của McBride.
Edward KMETT

2
Để mở rộng về "đọc cẩn thận ibind": Giới thiệu loại bí danh Atkey m i j a = m (a := j) i. Sử dụng điều này như mtrong định nghĩa của Atkey khôi phục hai chữ ký mà chúng tôi tìm kiếm: ireturnAtkin :: a -> m (a := i) iibindAtkin :: m (a := j) i -> (a -> m (b := k) j) -> m (b := k) i. Người đầu tiên là thu được bằng thành phần: ireturn . V. Cách thứ hai bằng cách (1) xây dựng một hàm forall j. (a := j) j -> m (b := k) jbằng cách so khớp mẫu, sau đó chuyển đối số được khôi phục ađến đối số thứ hai của ibindAtkin.
WorldSEnder

23

Như một kịch bản đơn giản, giả sử bạn có một đơn nguyên trạng thái. Loại trạng thái là một loại lớn phức tạp, nhưng tất cả các trạng thái này có thể được phân chia thành hai tập hợp: trạng thái đỏ và xanh lam. Một số hoạt động trong đơn nguyên này chỉ có ý nghĩa nếu trạng thái hiện tại là trạng thái màu xanh lam. Trong số này, một số sẽ giữ trạng thái màu xanh lam ( blueToBlue), trong khi những người khác sẽ làm cho trạng thái màu đỏ ( blueToRed). Trong một đơn nguyên thông thường, chúng tôi có thể viết

blueToRed  :: State S ()
blueToBlue :: State S ()

foo :: State S ()
foo = do blueToRed
         blueToBlue

gây ra lỗi thời gian chạy vì hành động thứ hai có trạng thái màu xanh lam. Chúng tôi muốn ngăn chặn điều này một cách tĩnh. Đơn nguyên được lập chỉ mục đáp ứng mục tiêu này:

data Red
data Blue

-- assume a new indexed State monad
blueToRed  :: State S Blue Red  ()
blueToBlue :: State S Blue Blue ()

foo :: State S ?? ?? ()
foo = blueToRed `ibind` \_ ->
      blueToBlue          -- type error

Lỗi loại được kích hoạt vì chỉ mục thứ hai của blueToRed( Red) khác với chỉ mục đầu tiên của blueToBlue( Blue).

Một ví dụ khác, với các đơn nguyên được lập chỉ mục, bạn có thể cho phép một đơn nguyên trạng thái thay đổi loại cho trạng thái của nó, ví dụ: bạn có thể có

data State old new a = State (old -> (new, a))

Bạn có thể sử dụng những điều trên để xây dựng một trạng thái là một ngăn xếp không đồng nhất được định kiểu tĩnh. Hoạt động sẽ có loại

push :: a -> State old (a,old) ()
pop  :: State (a,new) new a

Ví dụ khác, giả sử bạn muốn một đơn nguyên IO bị hạn chế không cho phép truy cập tệp. Bạn có thể sử dụng ví dụ:

openFile :: IO any FilesAccessed ()
newIORef :: a -> IO any any (IORef a)
-- no operation of type :: IO any NoAccess _

Bằng cách này, một kiểu có action IO ... NoAccess ()được đảm bảo về mặt tĩnh là không có quyền truy cập tệp. Thay vào đó, một hành động thuộc loại IO ... FilesAccessed ()có thể truy cập tệp. Có một đơn nguyên được lập chỉ mục có nghĩa là bạn không phải tạo một loại riêng biệt cho IO bị hạn chế, điều này sẽ yêu cầu sao chép mọi chức năng không liên quan đến tệp trong cả hai loại IO.


18

Đơn nguyên được lập chỉ mục không phải là đơn nguyên cụ thể, chẳng hạn như đơn nguyên trạng thái mà là một loại tổng quát của khái niệm đơn nguyên với các tham số kiểu bổ sung.

Trong khi giá trị đơn nguyên "chuẩn" có kiểu thì Monad m => m amột giá trị trong đơn nguyên được lập chỉ mục sẽ là IndexedMonad m => m i j anơi ijlà các loại chỉ mục để đó ilà loại chỉ mục ở đầu tính toán đơn nguyên và jở cuối quá trình tính toán. Theo một cách nào đó, bạn có thể icoi đây là một loại kiểu đầu vào và jkiểu đầu ra.

Sử dụng Statelàm ví dụ, một phép tính trạng thái State s aduy trì một trạng thái kiểu strong suốt quá trình tính toán và trả về kết quả kiểu a. Phiên bản được lập chỉ mục IndexedState i j a, là một phép tính trạng thái trong đó trạng thái có thể thay đổi thành một kiểu khác trong quá trình tính toán. Trạng thái ban đầu có kiểu ivà trạng thái và trạng thái kết thúc tính toán có kiểu j.

Việc sử dụng đơn nguyên được lập chỉ mục thay cho đơn nguyên bình thường hiếm khi cần thiết nhưng nó có thể được sử dụng trong một số trường hợp để mã hóa các đảm bảo tĩnh chặt chẽ hơn.


5

Điều quan trọng là phải xem cách lập chỉ mục được sử dụng trong các kiểu phụ thuộc (ví dụ: trong agda). Điều này có thể giải thích cách lập chỉ mục nói chung giúp ích như thế nào, sau đó chuyển trải nghiệm này sang đơn nguyên.

Lập chỉ mục cho phép thiết lập mối quan hệ giữa các trường hợp cụ thể của các loại. Sau đó, bạn có thể suy luận về một số giá trị để xác lập xem mối quan hệ đó có giữ được không.

Ví dụ (trong agda), bạn có thể chỉ định rằng một số số tự nhiên có liên quan với nhau _<_và kiểu cho biết chúng là những số nào. Sau đó, bạn có thể yêu cầu một số hàm được cung cấp một nhân chứng m < n, bởi vì chỉ khi đó hàm mới hoạt động chính xác - và nếu không cung cấp nhân chứng đó, chương trình sẽ không biên dịch.

Một ví dụ khác, với đủ sự kiên trì và hỗ trợ trình biên dịch cho ngôn ngữ bạn đã chọn, bạn có thể mã hóa rằng hàm giả định rằng một danh sách nhất định được sắp xếp.

Các monads được lập chỉ mục cho phép mã hóa một số hệ thống loại phụ thuộc hoạt động, để quản lý các tác dụng phụ chính xác hơ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.