Một đơn nguyên chỉ là một monoid trong thể loại endofunctor, vấn đề là gì?


723

Ai đầu tiên nói như sau?

Một đơn nguyên chỉ là một monoid trong thể loại endofunctor, vấn đề là gì?

Và trên một lưu ý ít quan trọng hơn, điều này có đúng không và nếu vậy bạn có thể đưa ra lời giải thích (hy vọng một điều có thể được hiểu bởi một người không có nhiều kinh nghiệm về Haskell)?


14
Xem "Danh mục dành cho nhà toán học làm việc"
Don Stewart

19
Bạn không cần phải hiểu điều này để sử dụng các đơn nguyên trong Haskell. Từ góc độ thực tế, chúng chỉ là một cách thông minh để vượt qua "trạng thái" thông qua một số hệ thống ống nước ngầm.
starblue

1
Tôi cũng muốn thêm bài đăng blog tuyệt vời này ở đây: stephendiehl.com/posts/monads.html Nó không trả lời trực tiếp câu hỏi, nhưng theo tôi, Stephen thực hiện một công việc tuyệt vời là buộc các danh mục và đơn vị trong Haskell lại với nhau. Nếu bạn đã đọc các câu trả lời ở trên - điều này sẽ giúp thống nhất hai cách nhìn này.
Ben Ford

3
Chính xác hơn "Đối với bất kỳ loại C nào, loại [C, C] của endofunctor của nó có cấu trúc đơn hình được tạo ra bởi chế phẩm. Một đối tượng đơn hình trong [C, C] là một đơn vị trên C." - từ en.wikipedia.org/wiki/Monoid_%28carget_theory%29. Xem en.wikipedia.org/wiki/Monad_%28carget_theory%29 để biết định nghĩa của đơn nguyên trong lý thuyết thể loại.

1
@Dmitry Một functor là một chức năng giữa các loại, với một số ràng buộc phải được xử lý tốt. Một endofunctor trên loại C chỉ là một functor từ C đến chính nó. Data.Functor là một kiểu chữ dành cho endofunctor trên danh mục Hask . Vì một thể loại bao gồm các đối tượng và hình thái, một functor cần ánh xạ cả hai. Đối với một ví dụ f của Data.Functor, bản đồ trên các đối tượng (loại haskell) là bản thân f và bản đồ về hình thái (hàm haskell) là fmap.
Matthijs

Câu trả lời:


796

Câu nói đặc biệt đó là của James Iry, từ Bản tóm tắt rất thú vị , không đầy đủ và chủ yếu là Lịch sử ngôn ngữ lập trình , trong đó ông giả tưởng nó là Philip Wadler.

Trích dẫn ban đầu là của Saunders Mac Lane trong Thể loại dành cho Nhà toán học làm việc , một trong những văn bản nền tảng của Lý thuyết Danh mục. Đây là bối cảnh , có lẽ là nơi tốt nhất để tìm hiểu chính xác ý nghĩa của nó.

Nhưng, tôi sẽ đâm. Câu gốc là:

Tất cả đã nói, một đơn vị trong X chỉ là một đơn chất trong danh mục endofunctor của X, với sản phẩm × được thay thế bằng thành phần của endofunctor và đơn vị được đặt bởi endofunctor.

X ở đây là một thể loại. Endofunctor là functor từ một thể loại đến chính nó (thường là tất cả Functor các liên quan đến lập trình viên chức năng, vì họ chủ yếu chỉ xử lý một loại; loại loại - nhưng tôi lạc đề). Nhưng bạn có thể tưởng tượng một thể loại khác là thể loại "endofunctor on X ". Đây là một thể loại trong đó các đối tượng là endofunctor và các hình thái là biến đổi tự nhiên.

Và trong số các endofunctor, một số trong số họ có thể là đơn nguyên. Những cái nào là đơn nguyên? Chính xác là những người đơn hình trong một ý nghĩa cụ thể. Thay vì đánh vần ánh xạ chính xác từ đơn nguyên sang đơn sắc (vì Mac Lane thực hiện điều đó tốt hơn nhiều so với tôi có thể hy vọng), tôi sẽ chỉ đặt các định nghĩa tương ứng của chúng cạnh nhau và để bạn so sánh:

Một monoid là ...

  • Một bộ, S
  • Một hoạt động, •: S × S → S
  • Một phần tử của S , e: 1 → S

... Đáp ứng các luật này:

  • (a • b) • c = a • (b • c) , với tất cả a , bc trong S
  • e • a = a • e = a , với tất cả a trong S

Một đơn nguyên là ...

  • Một endofunctor, T: X → X (trong Haskell, một hàm tạo kiểu * -> *Functorthể hiện)
  • Một sự biến đổi tự nhiên, μ: T × T → T , nơi × phương tiện sáng tác functor ( μ được gọi là jointrong Haskell)
  • Một sự biến đổi tự nhiên, η: Tôi → T , nơi tôi là endofunctor nhận dạng trên X ( η được gọi là returntrong Haskell)

... Đáp ứng các luật này:

  • μ ∘ Tμ = μ ∘ T
  • ∘ Tη = μ ∘ T = 1 (biến đổi tự nhiên danh tính)

Với một chút nheo mắt, bạn có thể thấy rằng cả hai định nghĩa này đều là các thể hiện của cùng một khái niệm trừu tượng .


21
cảm ơn đã giải thích và cảm ơn về bài viết Lịch sử lập trình ngắn gọn, không đầy đủ và chủ yếu là sai về ngôn ngữ lập trình. Tôi nghĩ rằng nó có thể là từ đó. Thực sự là một trong những phần lớn nhất của sự hài hước lập trình.
Roman A. Taycher

6
@Jonathan: Trong công thức cổ điển của một monoid, × có nghĩa là sản phẩm cartesian của các bộ. Bạn có thể đọc thêm về điều này ở đây: en.wikipedia.org/wiki/Cartesian_product , nhưng ý tưởng cơ bản là một phần tử của S × T là một cặp (s, t) , nơi s ∈ St ∈ T . Vì vậy, chữ ký của sản phẩm đơn hình •: S × S -> S trong ngữ cảnh này chỉ đơn giản có nghĩa là một hàm lấy 2 phần tử của S làm đầu vào và tạo ra một phần tử khác của S làm đầu ra.
Tom Crockett

12
@TahirHassan - Trong tổng quát của lý thuyết thể loại, chúng tôi xử lý các "đối tượng" mờ thay vì các tập hợp, và do đó không có khái niệm tiên nghiệm về "các yếu tố". Nhưng nếu bạn nghĩ về danh mục Đặt trong đó các đối tượng là tập hợp và mũi tên là các hàm, thì các phần tử của bất kỳ tập S nào đều tương ứng một-một với các hàm từ bất kỳ phần tử nào được đặt thành S. Đó là, đối với mọi phần tử e của S , có chính xác một hàm f: 1 -> S , trong đó 1 là bất kỳ tập hợp một phần tử nào ... (tiếp)
Tom Crockett

12
@TahirHassan Các tập hợp 1 phần tử tự chúng là chuyên môn hóa của khái niệm lý thuyết danh mục chung hơn về "đối tượng đầu cuối": một đối tượng đầu cuối là bất kỳ đối tượng nào thuộc danh mục có chính xác một mũi tên từ bất kỳ đối tượng nào khác với nó (bạn có thể kiểm tra xem điều này đúng với các bộ 1 phần tử trong Bộ ). Trong lý thuyết thể loại, các đối tượng đầu cuối được gọi đơn giản là 1 ; chúng là duy nhất cho đến đẳng cấu nên không có điểm nào phân biệt chúng. Vì vậy, bây giờ chúng ta có một mô tả lý thuyết thuần túy về "các yếu tố của S " cho bất kỳ S : chúng chỉ là các mũi tên từ 1 đến S !
Tom Crockett

7
@TahirHassan - Để đặt điều này theo thuật ngữ Haskell, hãy nghĩ về thực tế rằng nếu Slà một loại, tất cả những gì bạn có thể làm khi viết một hàm f :: () -> Slà chọn một số thuật ngữ cụ thể của loại S(một "phần tử" của nó, nếu bạn sẽ) nó ... bạn đã không được cung cấp thông tin thực sự với đối số, vì vậy không có cách nào để thay đổi hành vi của hàm. Vì vậy, fphải là một hàm hằng mà chỉ trả về cùng một điều mỗi lần. ()("Đơn vị") là đối tượng đầu cuối của loại Hask và không phải ngẫu nhiên mà có chính xác 1 giá trị (không phân kỳ) sinh sống ở đó.
Tom Crockett

532

Theo trực giác, tôi nghĩ rằng những gì các từ vựng toán học ưa thích đang nói là:

Monoid

Một monoid là một tập hợp các đối tượng và một phương pháp kết hợp chúng. Monoids nổi tiếng là:

  • số bạn có thể thêm
  • danh sách bạn có thể nối
  • bộ bạn có thể hợp nhất

Có nhiều ví dụ phức tạp hơn.

Hơn nữa, mọi monoid đều có một danh tính , đó là yếu tố "không-op" không có tác dụng khi bạn kết hợp nó với một thứ khác:

  • 0 + 7 == 7 + 0 == 7
  • [] ++ [1,2,3] == [1,2,3] ++ [] == [1,2,3]
  • {} union {apple} == {apple} union {} == {apple}

Cuối cùng, một monoid phải được liên kết . (bạn có thể giảm một chuỗi dài các kết hợp theo cách bạn muốn, miễn là bạn không thay đổi thứ tự từ trái sang phải của đối tượng) Bổ sung là OK ((5 + 3) +1 == 5+ (3+ 1)), nhưng phép trừ không ((5-3) -1! = 5- (3-1)).

Đơn nguyên

Bây giờ, hãy xem xét một loại tập hợp đặc biệt và một cách kết hợp các đối tượng đặc biệt.

Các đối tượng

Giả sử tập hợp của bạn chứa các đối tượng thuộc loại đặc biệt: hàm . Và các hàm này có một chữ ký thú vị: Chúng không mang số đến số hoặc chuỗi thành chuỗi. Thay vào đó, mỗi hàm mang một số đến một danh sách các số trong quy trình hai bước.

  1. Tính 0 hoặc nhiều kết quả
  2. Kết hợp những kết quả cho đến một câu trả lời nào đó.

Ví dụ:

  • 1 -> [1] (chỉ cần bọc đầu vào)
  • 1 -> [] (loại bỏ đầu vào, bọc sự trống rỗng trong danh sách)
  • 1 -> [2] (thêm 1 vào đầu vào và bọc kết quả)
  • 3 -> [4, 6] (thêm 1 vào đầu vào và nhân đầu vào với 2 và bọc nhiều kết quả )

Kết hợp các đối tượng

Ngoài ra, cách kết hợp các chức năng của chúng tôi là đặc biệt. Một cách đơn giản để kết hợp chức năng là thành phần : Hãy lấy các ví dụ của chúng tôi ở trên và tự soạn từng hàm:

  • 1 -> [1] -> [[1]] (bọc đầu vào, hai lần)
  • 1 -> [] -> [] (loại bỏ đầu vào, bọc khoảng trống trong danh sách, hai lần)
  • 1 -> [2] -> [UH-OH! ] (chúng tôi không thể "thêm 1" vào danh sách! ")
  • 3 -> [4, 6] -> [UH-OH! ] (chúng tôi không thể thêm 1 danh sách!)

Không cần quá nhiều vào lý thuyết loại, vấn đề là bạn có thể kết hợp hai số nguyên để có được một số nguyên, nhưng bạn không thể luôn luôn kết hợp hai hàm và có được một hàm cùng loại. (Các hàm có loại a -> a sẽ soạn, nhưng a-> [a] sẽ không.)

Vì vậy, hãy xác định một cách kết hợp các chức năng khác nhau. Khi chúng tôi kết hợp hai trong số các chức năng này, chúng tôi không muốn "nhân đôi" kết quả.

Đây là những gì chúng tôi làm. Khi chúng tôi muốn kết hợp hai chức năng F và G, chúng tôi thực hiện theo quy trình này (được gọi là ràng buộc ):

  1. Tính toán "kết quả" từ F nhưng không kết hợp chúng.
  2. Tính riêng các kết quả từ việc áp dụng G cho từng kết quả của F, thu được bộ sưu tập kết quả.
  3. Làm phẳng bộ sưu tập 2 cấp và kết hợp tất cả các kết quả.

Quay lại ví dụ của chúng tôi, hãy kết hợp (liên kết) một chức năng với chính nó bằng cách sử dụng phương thức "liên kết" mới này:

  • 1 -> [1] -> [1] (bọc đầu vào, hai lần)
  • 1 -> [] -> [] (loại bỏ đầu vào, bọc khoảng trống trong danh sách, hai lần)
  • 1 -> [2] -> [3] (thêm 1, sau đó thêm 1 lần nữa và bọc kết quả.)
  • 3 -> [4,6] -> [5,8,7,12] (thêm 1 vào đầu vào, và nhân bội đầu vào với 2, giữ cả hai kết quả, sau đó thực hiện lại tất cả cho cả hai kết quả, sau đó bọc cuối cùng kết quả trong một danh sách.)

Cách kết hợp các chức năng phức tạp hơn này là kết hợp (theo cách mà thành phần chức năng được kết hợp khi bạn không thực hiện các công cụ gói lạ mắt).

Buộc tất cả lại với nhau,

  • một đơn nguyên là một cấu trúc xác định cách kết hợp (kết quả của) các hàm,
  • tương tự như cách một monoid là một cấu trúc xác định cách kết hợp các đối tượng,
  • trong đó phương pháp kết hợp là kết hợp,
  • và nơi có một 'Không-op' đặc biệt có thể được kết hợp với bất kỳ thứ gì để dẫn đến một thứ gì đó không thay đổi.

Ghi chú

Có rất nhiều cách để "bọc" kết quả. Bạn có thể tạo một danh sách, hoặc một bộ, hoặc loại bỏ tất cả trừ kết quả đầu tiên trong khi lưu ý nếu không có kết quả, đính kèm một bên của trạng thái, in một thông điệp tường trình, v.v.

Tôi đã chơi một chút lỏng lẻo với các định nghĩa với hy vọng có được ý tưởng thiết yếu trên trực giác.

Tôi đã đơn giản hóa mọi thứ một chút bằng cách nhấn mạnh rằng đơn nguyên của chúng tôi hoạt động trên các chức năng loại a -> [a] . Trong thực tế, các đơn nguyên hoạt động trên các chức năng của loại a -> mb , nhưng khái quát hóa là một loại chi tiết kỹ thuật không phải là cái nhìn sâu sắc chính.


22
Đây là một lời giải thích hay về cách mọi đơn vị tạo thành một thể loại ( thể loại Kleisli là những gì bạn đang thể hiện - đó cũng là thể loại Eilenberg-Moore). Nhưng do thực tế là bạn không thể soạn bất kỳ hai mũi tên Kleisli nào a -> [b]c -> [d](bạn chỉ có thể làm điều này nếu b= c), điều này không hoàn toàn mô tả một đơn sắc. Đó thực sự là hoạt động làm phẳng mà bạn mô tả, chứ không phải là thành phần chức năng, là "toán tử đơn hình".
Tom Crockett

6
Cấp, nếu bạn giới hạn một đơn vị chỉ có một loại, tức là nếu bạn chỉ cho phép các mũi tên của Kleisli có dạng a -> [a], thì đây sẽ là một đơn thức (vì bạn sẽ giảm loại Kleisli thành một đối tượng và bất kỳ danh mục nào chỉ có một đối tượng theo định nghĩa là một monoid!), nhưng nó sẽ không nắm bắt được toàn bộ tổng quát của đơn nguyên.
Tom Crockett

5
Ở ghi chú cuối cùng, cần nhớ rằng, a -> [a] chỉ là một -> [] a. ([] cũng chỉ là hàm tạo kiểu.) Và vì vậy, nó không chỉ được xem là một -> mb, mà [] thực sự là một thể hiện của lớp Monad.
Evi1M4chine

8
Đây là lời giải thích tốt nhất và khó hiểu nhất về các đơn nguyên và nền tảng toán học của chúng về đơn chất mà tôi đã gặp trong vài tuần theo nghĩa đen. Đây là những gì nên được in trong mỗi cuốn sách Haskell khi nói về các đơn nguyên, xuống tay. LÊN TỚI! Có thể có thêm thông tin, rằng các đơn nguyên được nhận ra là các trường hợp kiểu chữ được tham số hóa bao bọc bất cứ thứ gì được đặt trong haskell, vào bài viết. (Ít nhất đó là cách tôi hiểu họ bây giờ. Hãy sửa tôi nếu tôi sai. Xem haskell.org/haskellwiki/What_a_Monad_is_not )
sjas

1
Điều này thật tuyệt vời - đó là lời giải thích duy nhất tôi hiểu đủ để có thể giải thích nó cho người khác ... Nhưng tôi vẫn không hiểu tại sao đây là một cách có giá trị để nghĩ về bất cứ điều gì. :(
Adam Barnes

84

Đầu tiên, các tiện ích mở rộng và thư viện mà chúng tôi sẽ sử dụng:

{-# LANGUAGE RankNTypes, TypeOperators #-}

import Control.Monad (join)

Trong số này, RankNTypeslà người duy nhất thực sự cần thiết cho bên dưới. Tôi đã từng viết một lời giải thích RankNTypesrằng một số người dường như đã thấy hữu ích , vì vậy tôi sẽ đề cập đến điều đó.

Trích dẫn câu trả lời tuyệt vời của Tom Crockett , chúng tôi có:

Một đơn nguyên là ...

  • Một endofunctor, T: X -> X
  • Một phép biến đổi tự nhiên ,: T × T -> T , trong đó × có nghĩa là thành phần functor
  • Một phép biến đổi tự nhiên ,: I -> T , trong đó tôi là endofunctor danh tính trên X

... Đáp ứng các luật này:

  • μ (μ (T × T) × T)) = μ (T × μ (T × T))
  • μ (η (T)) = T = μ (T (η))

Làm thế nào để chúng tôi dịch mã này sang mã Haskell? Chà, hãy bắt đầu với khái niệm về sự biến đổi tự nhiên :

-- | A natural transformations between two 'Functor' instances.  Law:
--
-- > fmap f . eta g == eta g . fmap f
--
-- Neat fact: the type system actually guarantees this law.
--
newtype f :-> g =
    Natural { eta :: forall x. f x -> g x }

Một loại hình thức f :-> gtương tự như một loại chức năng, nhưng thay vì nghĩ nó là một chức năng giữa hai loại (loại *), hãy nghĩ về nó như một hình thái giữa hai functor (mỗi loại * -> *). Ví dụ:

listToMaybe :: [] :-> Maybe
listToMaybe = Natural go
    where go [] = Nothing
          go (x:_) = Just x

maybeToList :: Maybe :-> []
maybeToList = Natural go
    where go Nothing = []
          go (Just x) = [x]

reverse' :: [] :-> []
reverse' = Natural reverse

Về cơ bản, trong Haskell, các phép biến đổi tự nhiên là các hàm từ loại này f xsang loại g xkhác sao cho xbiến loại là "không thể truy cập" đối với người gọi. Vì vậy, ví dụ, sort :: Ord a => [a] -> [a]không thể biến thành một phép biến đổi tự nhiên, bởi vì nó "kén chọn" về loại mà chúng ta có thể khởi tạo a. Một cách trực quan tôi thường sử dụng để nghĩ về điều này là:

  • Một functor là một cách hoạt động trên nội dung của một cái gì đó mà không cần chạm vào cấu trúc .
  • Một chuyển đổi tự nhiên là một cách hoạt động trên cấu trúc của một cái gì đó mà không cần chạm hoặc nhìn vào nội dung .

Bây giờ, với cách đó, hãy giải quyết các mệnh đề của định nghĩa.

Mệnh đề đầu tiên là "một endofunctor, T: X -> X. " Chà, mỗi người Functortrong Haskell là một endofunctor trong cái mà mọi người gọi là "thể loại Hask", có đối tượng là loại Haskell (loại *) và hình thái của họ là chức năng Haskell. Điều này nghe có vẻ như là một tuyên bố phức tạp, nhưng nó thực sự là một điều rất tầm thường. Tất cả điều đó có nghĩa là việc a Functor f :: * -> *cung cấp cho bạn phương tiện xây dựng một loại f a :: *cho bất kỳ a :: *và một chức năng fmap f :: f a -> f bnào trong số đó f :: a -> b, và những điều này tuân theo luật functor.

Mệnh đề thứ hai: Identityfunctor trong Haskell (đi kèm với Nền tảng, vì vậy bạn chỉ cần nhập nó) được định nghĩa theo cách này:

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
    fmap f (Identity a) = Identity (f a)

Vì vậy, phép biến đổi tự nhiên : I -> T từ định nghĩa của Tom Crockett có thể được viết theo cách này cho bất kỳ Monadtrường hợp nào t:

return' :: Monad t => Identity :-> t
return' = Natural (return . runIdentity)

Mệnh đề thứ ba: Thành phần của hai functor trong Haskell có thể được định nghĩa theo cách này (cũng đi kèm với Nền tảng):

newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two 'Functor's is also a 'Functor'.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose fga) = Compose (fmap (fmap f) fga)

Vì vậy, phép biến đổi tự nhiên : T × T -> T từ định nghĩa của Tom Crockett có thể được viết như sau:

join' :: Monad t => Compose t t :-> t
join' = Natural (join . getCompose)

Tuyên bố rằng đây là một monoid trong danh mục endofunctor sau đó có nghĩa là Compose(áp dụng một phần cho hai tham số đầu tiên của nó) có liên quan và đó Identitylà yếu tố nhận dạng của nó. Tức là, các đẳng cấu sau đây giữ:

  • Compose f (Compose g h) ~= Compose (Compose f g) h
  • Compose f Identity ~= f
  • Compose Identity g ~= g

Những điều này rất dễ chứng minh vì ComposeIdentitycả hai đều được định nghĩa là newtypevà Báo cáo Haskell định nghĩa ngữ nghĩa newtypelà sự đồng hình giữa kiểu được xác định và kiểu đối số với hàm tạo newtypedữ liệu của. Vì vậy, ví dụ, hãy chứng minh Compose f Identity ~= f:

Compose f Identity a
    ~= f (Identity a)                 -- newtype Compose f g a = Compose (f (g a))
    ~= f a                            -- newtype Identity a = Identity a
Q.E.D.

Trong Naturalnewtype, tôi không thể hiểu được (Functor f, Functor g)ràng buộc đang làm gì. Bạn có thể giải thích?
dfeuer 20/03/2015

@dfeuer Nó không thực sự làm bất cứ điều gì thiết yếu.
Luis Casillas

1
@LuisCasillas Tôi đã loại bỏ những Functorràng buộc đó vì chúng dường như không cần thiết. Nếu bạn không đồng ý thì hãy thêm lại chúng.
Lambda Fairy

Bạn có thể giải thích về ý nghĩa chính thức của sản phẩm functor được lấy làm thành phần không? Cụ thể, các hình thái chiếu cho thành phần functor là gì? Tôi đoán là sản phẩm chỉ được xác định cho functor F so với chính nó, F x F và chỉ khi joinđược xác định. Và đó joinlà hình thái chiếu. Nhưng tôi không chắc lắm.
tksfz

6

Lưu ý: Không, điều này không đúng. Tại một số thời điểm đã có một nhận xét về câu trả lời này từ chính Dan Piponi nói rằng nguyên nhân và kết quả ở đây hoàn toàn ngược lại, rằng ông đã viết bài báo của mình để đáp lại lời châm biếm của James Iry. Nhưng nó dường như đã được gỡ bỏ, có lẽ bởi một số người khó tính bắt buộc hơn.

Dưới đây là câu trả lời ban đầu của tôi.


Rất có khả năng Iry đã đọc từ Monoids đến Monads , một bài đăng trong đó Dan Piponi (sigfpe) bắt nguồn từ các monoids trong Haskell, với nhiều thảo luận về lý thuyết thể loại và đề cập rõ ràng về "thể loại endofunctor trên Hask ". Trong mọi trường hợp, bất cứ ai tự hỏi ý nghĩa của việc một đơn vị trở thành một monoid trong danh mục endofunctor có thể được hưởng lợi từ việc đọc dẫn xuất này.


1
"Có lẽ bởi một số người khó tính bắt buộc" - hoặc, như chúng tôi muốn đề cập đến họ trên trang web này, một người điều hành :-).
halfer

6

Tôi đến với bài viết này bằng cách hiểu rõ hơn suy luận của câu nói khét tiếng từ Lý thuyết hạng mục của Mac Lane dành cho nhà toán học làm việc .

Trong việc mô tả những gì là một cái gì đó, nó thường hữu ích như nhau để mô tả những gì nó không.

Thực tế là Mac Lane sử dụng mô tả để mô tả một Monad, người ta có thể ngụ ý rằng nó mô tả một cái gì đó độc đáo cho các đơn nguyên. Chịu đựng tôi đi. Để phát triển sự hiểu biết rộng hơn về tuyên bố, tôi tin rằng cần phải làm rõ rằng anh ta không mô tả một cái gì đó là duy nhất cho các đơn nguyên; tuyên bố mô tả như nhau về ứng dụng và mũi tên trong số những người khác. Vì lý do tương tự, chúng tôi có thể có hai đơn sắc trên Int (Tổng và Sản phẩm), chúng tôi có thể có một số đơn vị trên X trong danh mục của endofunctor. Nhưng thậm chí còn có nhiều điểm tương đồng.

Cả Monad và Applicative đều đáp ứng các tiêu chí:

  • endo => bất kỳ mũi tên, hoặc hình thái bắt đầu và kết thúc ở cùng một nơi
  • functor => bất kỳ mũi tên, hoặc hình thái giữa hai loại

    (ví dụ: hàng ngày Tree a -> List b, nhưng trong Danh mục Tree -> List)

  • đơn hình => đối tượng đơn lẻ; tức là một loại duy nhất, nhưng trong bối cảnh này, chỉ liên quan đến lớp bên ngoài; vì vậy, chúng ta không thể có Tree -> List, chỉ có List -> List.

Câu lệnh sử dụng "Danh mục ..." Điều này xác định phạm vi của câu lệnh. Như một ví dụ, các functor Thể loại mô tả phạm vi f * -> g *, tức là Any functor -> Any functor, ví dụ, Tree * -> List *hoặc Tree * -> Tree *.

Những gì một tuyên bố phân loại không chỉ định mô tả nơi mọi thứ và mọi thứ được cho phép .

Trong trường hợp này, bên trong functor, * -> *aka a -> bkhông được chỉ định có nghĩa là gì Anything -> Anything including Anything else. Khi trí tưởng tượng của tôi nhảy đến Int -> String, nó cũng bao gồm Integer -> Maybe Int, hoặc thậm chí là Maybe Double -> Either String Intở đâu a :: Maybe Double; b :: Either String Int.

Vì vậy, tuyên bố đi kèm với nhau như sau:

  • phạm vi functor :: f a -> g b(nghĩa là bất kỳ loại tham số hóa cho bất kỳ loại tham số hóa)
  • endo + functor :: f a -> f b(nghĩa là, bất kỳ một loại tham số hóa nào cho cùng một loại tham số hóa) ... nói khác nhau,
  • một monoid trong thể loại endofunctor

Vậy, sức mạnh của công trình này ở đâu? Để đánh giá đầy đủ tính năng động, tôi cần phải thấy rằng các bản vẽ điển hình của một đơn hình (một vật thể trông giống như một mũi tên nhận dạng :: single object -> single object), không minh họa rằng tôi được phép sử dụng một mũi tên được tham số hóa với bất kỳ số lượng giá trị đơn trị nào, từ đối tượng một loại được phép trong Monoid. Định nghĩa mũi tên endo, ~ nhận dạng tương đương bỏ qua giá trị loại của functor và cả loại và giá trị của lớp "tải trọng" bên trong nhất. Do đó, trả lại tương đương truetrong bất kỳ tình huống nào mà các loại functorial khớp (ví dụ, Nothing -> Just * -> Nothingtương đương với Just * -> Just * -> Just *vì cả hai đều là Maybe -> Maybe -> Maybe).

Thanh bên: ~ bên ngoài là khái niệm, nhưng là biểu tượng bên trái nhất f a. Nó cũng mô tả những gì "Haskell" đọc - đầu tiên (ảnh lớn); vì vậy Loại là "bên ngoài" liên quan đến Giá trị Loại. Mối quan hệ giữa các lớp (một chuỗi các tham chiếu) trong lập trình không dễ liên quan trong Danh mục. Danh mục Tập hợp được sử dụng để mô tả Các loại (Int, Chuỗi, Có thể Int, v.v.) bao gồm Danh mục Functor (Các loại tham số). Chuỗi tham chiếu: Loại Functor, giá trị Functor (các phần tử của tập hợp Functor đó, ví dụ: Không có gì, Chỉ) và lần lượt, mọi thứ khác mà mỗi giá trị functor trỏ đến. Trong Danh mục, mối quan hệ được mô tả khác nhau, ví dụ: return :: a -> m ađược coi là một phép biến đổi tự nhiên từ một Functor sang một Functor khác, khác với mọi thứ được đề cập cho đến nay.

Quay lại chủ đề chính, tất cả, đối với bất kỳ sản phẩm tenxơ xác định nào và giá trị trung tính, tuyên bố kết thúc mô tả một cấu trúc tính toán mạnh mẽ đáng kinh ngạc sinh ra từ cấu trúc nghịch lý của nó:

  • ở bên ngoài nó xuất hiện dưới dạng một đối tượng (ví dụ :: List:); tĩnh
  • nhưng bên trong, cho phép rất nhiều động lực
    • bất kỳ số lượng giá trị nào cùng loại (ví dụ: Empty | ~ NonEmpty) dưới dạng thức ăn cho các chức năng của bất kỳ arity nào. Sản phẩm tenor sẽ giảm bất kỳ số lượng đầu vào nào xuống một giá trị ... cho lớp bên ngoài (~ foldkhông nói gì về tải trọng)
    • phạm vi vô hạn của cả loại và giá trị cho lớp bên trong nhất

Trong Haskell, làm rõ khả năng áp dụng của tuyên bố là rất quan trọng. Sức mạnh và tính linh hoạt của cấu trúc này, đã hoàn toàn không có gì để làm với một đơn nguyên cho mỗi gia nhập . Nói cách khác, cấu trúc không dựa vào những gì làm cho một đơn nguyên trở nên độc đáo.

Khi cố gắng tìm hiểu xem có nên xây dựng mã với bối cảnh được chia sẻ để hỗ trợ các tính toán phụ thuộc lẫn nhau hay không, so với các tính toán có thể chạy song song, câu lệnh khét tiếng này, với nhiều như nó mô tả, không phải là sự tương phản giữa sự lựa chọn của Áp dụng, Mũi tên và Monads, nhưng đúng hơn là một mô tả về mức độ chúng giống nhau. Đối với các quyết định trong tay, tuyên bố là moot.

Điều này thường bị hiểu lầm. Tuyên bố tiếp tục được mô tả join :: m (m a) -> m anhư là sản phẩm tenor cho endofunctor đơn hình. Tuy nhiên, nó không nói rõ làm thế nào, trong bối cảnh của tuyên bố này, (<*>)cũng có thể đã được chọn. Nó thực sự là một ví dụ về sáu / nửa tá. Logic để kết hợp các giá trị hoàn toàn giống nhau; cùng một đầu vào tạo ra cùng một đầu ra từ mỗi (không giống như các đơn chất Sum và Sản phẩm cho Int vì chúng tạo ra các kết quả khác nhau khi kết hợp Ints).

Vì vậy, để tóm tắt lại: Một monoid trong danh mục endofunctor mô tả:

   ~t :: m * -> m * -> m *
   and a neutral value for m *

(<*>)(>>=)cả hai đều cung cấp quyền truy cập đồng thời vào hai mgiá trị để tính giá trị trả về đơn. Logic được sử dụng để tính giá trị trả về là hoàn toàn giống nhau. Nếu không phải là các hình dạng khác nhau của các hàm mà chúng tham số hóa ( f :: a -> bso với k :: a -> m b) và vị trí của tham số có cùng kiểu trả về tính toán (nghĩa là a -> b -> bso với b -> a -> btừng hàm tương ứng), tôi nghi ngờ chúng ta có thể đã tham số hóa logic đơn hình, sản phẩm tenor, để tái sử dụng trong cả hai định nghĩa. Là một bài tập để đưa ra quan điểm, hãy thử và thực hiện ~t, và bạn kết thúc với (<*>)(>>=)tùy thuộc vào cách bạn quyết định xác định nó forall a b.

Nếu điểm cuối cùng của tôi là tối thiểu đúng về mặt khái niệm, thì nó sẽ giải thích sự khác biệt chính xác và duy nhất về tính toán giữa Applicative và Monad: các chức năng mà chúng tham số hóa. Nói cách khác, sự khác biệt là bên ngoài đối với việc thực hiện các lớp loại này.

Tóm lại, theo kinh nghiệm của riêng tôi, câu nói nổi tiếng của Mac Lane đã cung cấp một meme "goto" tuyệt vời, một hướng dẫn để tôi tham khảo trong khi điều hướng theo cách của tôi thông qua Danh mục để hiểu rõ hơn các thành ngữ được sử dụng trong Haskell. Nó thành công trong việc nắm bắt phạm vi của một khả năng tính toán mạnh mẽ có thể truy cập tuyệt vời trong Haskell.

Tuy nhiên, có một điều trớ trêu là lần đầu tiên tôi hiểu nhầm khả năng áp dụng của tuyên bố bên ngoài đơn nguyên và những gì tôi hy vọng được truyền đạt ở đây. Tất cả mọi thứ mà nó mô tả hóa ra là những gì giống nhau giữa Applicative và Monads (và Mũi tên giữa những người khác). Những gì nó không nói chính xác là sự phân biệt nhỏ nhưng hữu ích giữa chúng.

- E


5

Các câu trả lời ở đây thực hiện một công việc tuyệt vời trong việc xác định cả đơn âm và đơn âm, tuy nhiên, dường như chúng vẫn không trả lời được câu hỏi:

Và trên một lưu ý ít quan trọng hơn, điều này có đúng không và nếu vậy bạn có thể đưa ra lời giải thích (hy vọng một điều có thể được hiểu bởi một người không có nhiều kinh nghiệm về Haskell)?

Mấu chốt của vấn đề còn thiếu ở đây, là khái niệm khác nhau về "monoid", cái gọi là phân loại chính xác hơn - một trong những đơn chất trong một thể loại đơn. Đáng buồn thay, cuốn sách của Mac Lane khiến nó rất khó hiểu :

Tất cả đã nói, một đơn nguyên Xchỉ là một monoid trong danh mục endofunctor của X, với sản phẩm được ×thay thế bằng thành phần của endofunctor và đơn vị được thiết lập bởi endofunctor nhận dạng.

Sự nhầm lẫn chính

Tại sao điều này khó hiểu? Bởi vì nó không định nghĩa "monoid trong danh mục endofunctor" là gì X. Thay vào đó, câu này đề nghị lấy một monoid bên trong tập hợp tất cả các endofunctor cùng với thành phần functor là hoạt động nhị phân và functor nhận dạng như một đơn vị đơn hình. Nó hoạt động hoàn toàn tốt và biến thành một tập hợp con bất kỳ tập hợp con endofunctor có chứa functor danh tính và được đóng dưới thành phần functor.

Tuy nhiên, đây không phải là giải thích chính xác, mà cuốn sách không làm rõ ở giai đoạn đó. Một Monad flà một endofunctor cố định , không phải là một tập hợp con endofunctor được đóng theo thành phần. Một cấu trúc phổ biến là sử dụng fđể tạo ra một monoid bằng cách lấy tập hợp các kthành phần f^k = f(f(...))gồm nhiều phần của fchính nó, bao gồm cả phần k=0tương ứng với danh tính f^0 = id. Và bây giờ, tập hợp Stất cả các quyền hạn k>=0này thực sự là một "đơn chất" với sản phẩm × được thay thế bằng thành phần của endofunctor và đơn vị được thiết lập bởi endofunctor danh tính ".

Chưa hết:

  • Monoid này Scó thể được định nghĩa cho bất kỳ functor fhoặc thậm chí theo nghĩa đen cho bất kỳ bản đồ tự X. Nó là monoid được tạo ra bởi f.
  • Cấu trúc đơn hình Sđược đưa ra bởi thành phần functor và functor danh tính không liên quan gì đến việc có fhay không là một đơn nguyên.

Và để làm cho mọi thứ trở nên khó hiểu hơn, định nghĩa của "monoid trong thể loại đơn" xuất hiện sau trong cuốn sách như bạn có thể thấy từ mục lục . Tuy nhiên, hiểu được khái niệm này là rất quan trọng để hiểu mối liên hệ với các đơn nguyên.

(Nghiêm) các thể loại đơn

Chuyển đến Chương VII trên Monoids (xuất hiện muộn hơn Chương VI trên Monads), chúng tôi thấy định nghĩa của loại được gọi là loại đơn hình nghiêm ngặt là ba (B, *, e), trong đó Bmột loại, *: B x B-> Bmột bifunctor (functor đối với từng thành phần với thành phần khác được cố định ) và elà một đối tượng đơn vị trong B, đáp ứng luật kết hợp và luật đơn vị:

(a * b) * c = a * (b * c)
a * e = e * a = a

cho bất kỳ đối tượng a,b,c của B, và bản sắc tương tự cho bất kỳ morphisms a,b,cvới ethay thế bằng id_e, các cấu xạ sắc của e. Bây giờ chúng ta nên quan sát rằng trong trường hợp quan tâm của chúng ta, đâu Blà danh mục của endofunctor Xvới các biến đổi tự nhiên là hình thái, *thành phần functor và efunctor, tất cả các luật này đều được thỏa mãn, như có thể được xác minh trực tiếp.

Điều xuất hiện sau trong cuốn sách là định nghĩa về phạm trù đơn hình "thoải mái" , trong đó các luật chỉ giữ modulo một số biến đổi tự nhiên cố định thỏa mãn cái gọi là quan hệ kết hợp , tuy nhiên không quan trọng đối với các trường hợp của chúng ta về các thể loại endofunctor.

Đơn chất trong các loại đơn

Cuối cùng, trong phần 3 "Monoids" của Chương VII, định nghĩa thực tế được đưa ra:

Một monoid ctrong một thể loại đơn(B, *, e) là một đối tượng Bcó hai mũi tên (hình thái)

mu: c * c -> c
nu: e -> c

làm 3 sơ đồ giao hoán. Hãy nhớ lại rằng trong trường hợp của chúng tôi, đây là những hình thái trong thể loại endofunctor, là những biến đổi tự nhiên tương ứng với chính xác joinreturn cho một đơn nguyên. Sự kết nối càng trở nên rõ ràng hơn khi chúng ta làm cho bố cục *rõ ràng hơn, thay thế c * cbằng c^2, cđơn vị của chúng ta ở đâu .

Cuối cùng, lưu ý rằng 3 sơ đồ giao hoán (theo định nghĩa của một đơn hình trong loại đơn hình) được viết cho các loại đơn hình chung (không nghiêm ngặt), trong khi trong trường hợp của chúng tôi, tất cả các biến đổi tự nhiên phát sinh như một phần của thể loại đơn hình thực sự là danh tính. Điều đó sẽ làm cho các sơ đồ giống hệt như các sơ đồ trong định nghĩa của một đơn nguyên, làm cho sự tương ứng hoàn thành.

Phần kết luận

Tóm lại, bất kỳ đơn nguyên nào theo định nghĩa là endofunctor, do đó, một đối tượng trong danh mục endofunctor, trong đó đơn vị joinreturntoán tử thỏa mãn định nghĩa của một monoid trong thể loại đơn (cụ thể) cụ thể đó . Ngược lại, bất kỳ monoid nào trong thể loại endofunctor theo định nghĩa là một bộ ba (c, mu, nu)bao gồm một đối tượng và hai mũi tên, ví dụ như các phép biến đổi tự nhiên trong trường hợp của chúng ta, đáp ứng các định luật giống như một đơn nguyên.

Cuối cùng, lưu ý sự khác biệt chính giữa các đơn sắc (cổ điển) và các đơn sắc tổng quát hơn trong các thể loại đơn. Hai mũi tên munutrên không nữa một hoạt động nhị phân và một đơn vị trong một bộ. Thay vào đó, bạn có một endofunctor cố định c. Thành phần functor* và functor danh tính không cung cấp cấu trúc hoàn chỉnh cần thiết cho đơn nguyên, mặc dù nhận xét khó hiểu trong cuốn sách.

Một cách tiếp cận khác là so sánh với monoid tiêu chuẩn Ccủa tất cả các bản đồ tự của một tập hợp A, trong đó hoạt động nhị phân là thành phần, có thể được nhìn thấy để ánh xạ sản phẩm cartesian tiêu chuẩn C x Cvào C. Chuyển đến monoid được phân loại, chúng tôi sẽ thay thế sản phẩm cartesian xbằng thành phần functor *và hoạt động nhị phân được thay thế bằng chuyển đổi tự nhiên mutừ c * csang c, đó là một tập hợp các jointoán tử

join: c(c(T))->c(T)

cho mọi đối tượng T(gõ trong lập trình). Và các yếu tố nhận dạng trong các đơn sắc cổ điển, có thể được xác định bằng hình ảnh của các bản đồ từ tập hợp một điểm cố định, được thay thế bằng bộ sưu tập của các returntoán tử

return: T->c(T) 

Nhưng bây giờ không còn sản phẩm cartesian, vì vậy không có cặp phần tử và do đó không có hoạt động nhị phân.


Vì vậy, câu trả lời của bạn cho phần "đây có phải là sự thật" của câu hỏi là gì? Có đúng là một đơn nguyên là một monoid trong thể loại endofunctor? Và nếu có, mối quan hệ giữa khái niệm lý thuyết thể loại của một monoid và một monoid đại số (một tập hợp với một phép nhân liên kết và một đơn vị) là gì?
Alexander Belopolsky
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.