Người đăng ký soạn, đơn không


110

Người đăng ký soạn, đơn nguyên thì không.

Câu nói trên có nghĩa là gì? Và khi nào thì cái này thích hơn cái khác?


5
Bạn lấy câu nói này từ đâu? Có thể hữu ích khi xem một số ngữ cảnh.
fuz

@FUZxxl: Tôi đã nghe nhiều lần nó từ nhiều người khác nhau, gần đây là từ debasishg trên twitter.
missfaktor

3
@stephen tetley: Lưu ý rằng nhiều Applicatives như vậy thực sự là một họ toàn bộ của Monads, cụ thể là một cho mỗi "hình dạng" của cấu trúc có thể. ZipListkhông phải là a Monad, nhưng ZipLists có độ dài cố định là. Readerlà một trường hợp đặc biệt thuận tiện (hay là tổng quát?) trong đó kích thước của "cấu trúc" được cố định làm bản chất của kiểu môi trường.
CA McCann

3
@CAMcCann Tất cả các ứng dụng linh hoạt đó (cho dù chúng cắt ngắn hay đệm) đều hạn chế ở các Readerđơn nguyên nếu bạn sửa hình dạng theo cách tương đương với đơn nguyên tối đa là đẳng cấu. Khi bạn cố định hình dạng của một vùng chứa, nó sẽ mã hóa một cách hiệu quả một hàm từ các vị trí, chẳng hạn như một bộ nhớ ghi nhớ. Peter Hancock gọi những kẻ vui nhộn như vậy là "Naperian", vì chúng tuân theo luật logarit.
pigworker

4
@stephen tetley: Các ví dụ khác bao gồm ứng dụng đơn nguyên không đổi (là một thành phần của đơn nguyên nhưng không phải là đơn nguyên) và ứng dụng đơn vị trễ (tốt hơn là không thừa nhận tham gia).
pigworker

Câu trả lời:


115

Nếu chúng ta so sánh các loại

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

chúng tôi nhận được manh mối về những gì phân tách hai khái niệm. Điều đó (s -> m t)trong loại (>>=)cho thấy rằng một giá trị trong scó thể xác định hành vi của một phép tính trong m t. Đơn nguyên cho phép giao thoa giữa các lớp giá trị và tính toán. Các (<*>)nhà điều hành cho phép không có sự can thiệp như: chức năng và lập luận tính toán không phụ thuộc vào giá trị. Điều này thực sự cắn. Đối chiếu

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

trong đó sử dụng kết quả của một số hiệu ứng để quyết định giữa hai phép tính (ví dụ: phóng tên lửa và ký hiệp định đình chiến), trong khi

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

trong đó sử dụng giá trị của abđể lựa chọn giữa các giá trị của hai phép tính ataf, đã thực hiện cả hai, có lẽ gây ra hậu quả bi thảm.

Phiên bản đơn nguyên về cơ bản dựa vào sức mạnh bổ sung của (>>=)việc chọn phép tính từ một giá trị và điều đó có thể quan trọng. Tuy nhiên, việc hỗ trợ sức mạnh đó khiến các monads khó có thể soạn thảo. Nếu chúng tôi cố gắng xây dựng 'liên kết kép'

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

chúng ta đi được xa đến mức này, nhưng bây giờ các lớp của chúng ta đều bị lộn xộn. Chúng ta có một n (m (n t)), vì vậy chúng ta cần phải loại bỏ cái bên ngoài n. Như Alexandre C nói, chúng ta có thể làm điều đó nếu chúng ta có

swap :: n (m t) -> m (n t)

để hoán vị phần ntrong và phần bên trong joinvới phần kia n.

'Áp dụng kép' yếu hơn dễ xác định hơn nhiều

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

vì không có sự giao thoa giữa các lớp.

Tương ứng, thật tốt để nhận ra khi nào bạn thực sự cần thêm sức mạnh của Monads và khi nào bạn có thể thoát khỏi cấu trúc tính toán cứng nhắc Applicativehỗ trợ.

Nhân tiện, xin lưu ý rằng mặc dù việc soạn thảo các monads rất khó, nhưng nó có thể nhiều hơn mức bạn cần. Kiểu m (n v)chỉ ra tính toán với m-effects, sau đó tính toán với n-effects thành -value v, trong đó m-effects kết thúc trước khi n-effects bắt đầu (do đó cần swap). Nếu bạn chỉ muốn xen kẽ m-effects với n-effects, thì bố cục có lẽ là quá nhiều để yêu cầu!


3
Đối với ví dụ iffy, bạn tuyên bố rằng nó "sử dụng giá trị của ab để chọn giữa các giá trị của hai phép tính at và af, đã thực hiện cả hai, có lẽ dẫn đến hậu quả đáng tiếc." Bản chất lười biếng của Haskell không bảo vệ bạn chống lại điều này sao? If I have list = (\ btf -> if b then t else f): [] và sau đó thực hiện câu lệnh: list <*> pure True <*> pure "hello" <*> pure (error "bad"). ... Tôi nhận được "xin chào" và lỗi không bao giờ xảy ra. Mã này gần như không an toàn hoặc được kiểm soát như một đơn nguyên, nhưng bài đăng có vẻ như nó gợi ý rằng các ứng dụng gây ra đánh giá nghiêm ngặt. Nhìn chung bài tuyệt vời mặc dù! Cảm ơn!
shj

7
Bạn vẫn nhận được hiệu ứng của cả hai, nhưng thuần túy (lỗi "xấu") không có bất kỳ tác dụng nào. Mặt khác, nếu bạn thử iffy (pure True) ("hello" thuần túy) (lỗi "bad"), bạn sẽ gặp lỗi mà miffy tránh được. Hơn nữa, nếu bạn thử một cái gì đó như iffy (pure True) (pure 0) [1,2], bạn sẽ nhận được [0,0] thay vì [0]. Những người áp dụng có một kiểu nghiêm ngặt về chúng, ở chỗ họ xây dựng các chuỗi tính toán cố định, nhưng các giá trị thu được từ các phép tính đó vẫn được kết hợp một cách lười biếng, như bạn quan sát.
pigworker

Có đúng không, đối với bất kỳ đơn nguyên nào mnbạn luôn có thể viết biến áp đơn nguyên mtvà vận hành n (m t)sử dụng mt n t? Vì vậy, bạn luôn có thể soạn monads, nó chỉ phức tạp hơn, sử dụng máy biến áp?
ron

4
Những máy biến áp như vậy thường tồn tại, nhưng theo tôi biết, không có cách nào để tạo ra chúng. Thường có sự lựa chọn chính xác về cách giải quyết các hiệu ứng xen kẽ từ các monads khác nhau, ví dụ cổ điển là ngoại lệ và trạng thái. Một ngoại lệ có nên thay đổi trạng thái quay lại hay không? Cả hai lựa chọn đều có vị trí của chúng. Phải nói rằng, có một thứ "đơn nguyên tự do" thể hiện sự "đan xen tùy tiện". data Free f x = Ret x | Do (f (Free f x)), sau đó data (:+:) f g x = Inl (f x) | Tnr (g x), và xem xét Free (m :+: n). Điều đó làm trì hoãn sự lựa chọn cách chạy xen kẽ.
pigworker

@pigworker Liên quan đến cuộc tranh luận lười biếng / nghiêm khắc. Tôi nghĩ rằng với các ứng dụng, bạn không thể kiểm soát hiệu ứng từ bên trong tính toán, nhưng lớp hiệu ứng rất có thể quyết định không đánh giá các giá trị sau này. Đối với trình phân tích cú pháp (ứng dụng), điều này có nghĩa là nếu trình phân tích cú pháp không thành công sớm, các trình phân tích cú pháp tiếp theo sẽ không được đánh giá / áp dụng cho đầu vào. Vì Maybeđiều này có nghĩa là một sớm Nothingsẽ ngăn chặn việc đánh giá acủa một muộn hơn / tiếp theo Just a. Điều này có chính xác?
ziggystar

75

Người đăng ký soạn, đơn nguyên thì không.

Đơn nguyên có thể soạn, nhưng kết quả có thể không phải là đơn nguyên. Ngược lại, thành phần của hai ứng dụng nhất thiết phải là một ứng dụng. Tôi nghi ngờ ý định của tuyên bố ban đầu là "Tính ứng dụng là sáng tác, trong khi tính đơn nguyên thì không." Được diễn đạt lại, " Applicativeđược đóng theo thành phần, và Monadkhông."


24
Ngoài ra, bất kỳ hai ứng dụng nào tạo thành một cách hoàn toàn cơ học, trong khi đơn nguyên được tạo thành bởi thành phần của hai đơn nguyên là cụ thể cho thành phần đó.
Apocalisp

12
Hơn nữa đơn nguyên sáng tác theo cách khác, tích của hai đơn nguyên là một đơn nguyên, nó chỉ là những sản phẩm đồng cấu cần một số loại luật phân phối.
Edward KMETT

Với, @Apocalisp, kèm theo bình luận, đây là câu trả lời ngắn gọn và tốt nhất.
Paul Draper

39

Nếu bạn có các ứng dụng A1A2thì kiểu đó data A3 a = A3 (A1 (A2 a))cũng là ứng dụng (bạn có thể viết một ví dụ như vậy theo cách chung chung).

Mặt khác, nếu bạn có đơn nguyên M1M2thì kiểu đó data M3 a = M3 (M1 (M2 a))không nhất thiết phải là đơn nguyên (không có cách triển khai chung hợp lý cho >>=hoặc joincho bố cục).

Một ví dụ có thể là kiểu [Int -> a](ở đây chúng ta soạn một hàm tạo kiểu []với (->) Int, cả hai đều là đơn nguyên). Bạn có thể dễ dàng viết

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

Và điều đó khái quát cho bất kỳ ứng dụng nào:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

Nhưng không có định nghĩa hợp lý về

join :: [Int -> [Int -> a]] -> [Int -> a]

Nếu bạn không tin vào điều này, hãy xem xét biểu thức này:

join [\x -> replicate x (const ())]

Độ dài của danh sách trả về phải được đặt bằng đá trước khi cung cấp một số nguyên, nhưng độ dài chính xác của nó phụ thuộc vào số nguyên được cung cấp. Do đó, không có joinchức năng chính xác nào có thể tồn tại cho loại này.


1
... vì vậy tránh các monads khi một hàm sẽ làm gì?
andrew cooke

2
@andrew, nếu bạn muốn nói đến functor, thì đúng vậy, functor đơn giản hơn và nên được sử dụng khi đủ. Lưu ý rằng không phải lúc nào cũng vậy. Ví dụ IOnếu không có một Monadsẽ rất khó lập trình. :)
Rotsor

17

Thật không may, mục tiêu thực sự của chúng tôi, thành phần của các monads, khá khó khăn hơn. .. Trong thực tế, chúng ta có thể thực sự chứng minh rằng, ở một khía cạnh nào đó, không có cách nào để xây dựng một hàm nối với kiểu trên mà chỉ sử dụng các phép toán của hai đơn thức (xem phần phụ lục để có dàn ý chứng minh). Theo đó, cách duy nhất mà chúng ta có thể hy vọng để tạo thành một bố cục là nếu có một số cấu trúc bổ sung liên kết hai thành phần.

Soạn monads, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf


4
Tl; dr cho những độc giả thiếu kiên nhẫn: bạn có thể soạn các monads nếu (f?) Bạn có thể cung cấp một sự chuyển đổi tự nhiênswap : N M a -> M N a
Alexandre C.

@Alexandre C.: Chỉ là "nếu", tôi nghi ngờ. Không phải tất cả các máy biến áp đơn nguyên đều được mô tả bằng cấu tạo bộ phễu trực tiếp. Ví dụ, ContT r m akhông phải m (Cont r a)cũng không Cont r (m a), và StateT s m alà đại khái Reader s (m (Writer s a)).
CA McCann

@CA McCann: Tôi dường như không thể chuyển từ (đơn nguyên M, đơn nguyên N, đơn nguyên MN, đơn nguyên NM) sang (tồn tại hoán đổi: MN -> NM tự nhiên). Vì vậy, hãy gậy để "nếu" cho bây giờ (có lẽ câu trả lời là trong bài báo này, tôi phải thú nhận tôi nhìn nó một cách nhanh chóng)
Alexandre C.

1
@Alexandre C.: Dù sao chỉ xác định rằng các tác phẩm là đơn nguyên có thể là không đủ - bạn cũng cần một số cách để liên kết hai phần với tổng thể. Sự tồn tại của swaphàm ý rằng bố cục cho phép cả hai "hợp tác" bằng cách nào đó. Ngoài ra, lưu ý rằng đó sequencelà một trường hợp đặc biệt của "hoán đổi" đối với một số monads. Thực ra cũng vậy flip.
CA McCann

7
Để viết, swap :: N (M x) -> M (N x)tôi có vẻ như bạn có thể sử dụng returns(phù hợp fmapped) để chèn một Mở phía trước và một Nở phía sau, đi từ N (M x) -> M (N (M (N x))), sau đó sử dụng jointổng hợp để lấy M (N x).
pigworker

7

Giải luật phân phối l: MN -> NM là đủ

để đảm bảo tính đơn nguyên của NM. Để thấy điều này, bạn cần một đơn vị và một mult. tôi sẽ tập trung vào mult (đơn vị là unit_N unitM)

NMNM - l -> NNMM - mult_N mult_M -> NM

Điều này không đảm bảo rằng MN là một đơn nguyên.

Tuy nhiên, quan sát quan trọng sẽ phát huy tác dụng khi bạn có các giải pháp luật phân phối

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

do đó, LM, LN và MN là các đơn nguyên. Câu hỏi đặt ra là liệu LMN có phải là một đơn nguyên hay không (bởi

(MN) L -> L (MN) hoặc bằng N (LM) -> (LM) N

Chúng tôi có đủ cấu trúc để tạo ra những bản đồ này. Tuy nhiên, như Eugenia Cheng quan sát , chúng ta cần một điều kiện lục giác (tương đương với việc trình bày phương trình Yang-Baxter) để đảm bảo tính đơn nguyên của một trong hai công trình. Thực ra, với điều kiện lục thức, hai đơn nguyên khác nhau lại trùng khớp.


9
Đây có lẽ là một câu trả lời tuyệt vời, nhưng nó đã đi bất ngờ tới thăm cách trên đầu của tôi.
Dan Burton

1
Đó là bởi vì, sử dụng thuật ngữ Ứng dụng và thẻ haskell, đây là một câu hỏi về haskell nhưng với câu trả lời bằng một ký hiệu khác.
codehot
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.