Xin lỗi, tôi thực sự không biết toán học của mình, vì vậy tôi tò mò muốn biết cách phát âm các hàm trong kiểu chữ Ứng dụng
Tôi nghĩ rằng việc biết toán của bạn hay không, phần lớn không liên quan. Như bạn có thể đã biết, Haskell vay mượn một vài thuật ngữ từ các lĩnh vực toán học trừu tượng khác nhau, đáng chú ý nhất là Lý thuyết phạm trù , từ đó chúng ta có được các hàm và đơn nguyên. Việc sử dụng các thuật ngữ này trong Haskell hơi khác với các định nghĩa toán học chính thức, nhưng chúng thường đủ gần để trở thành các thuật ngữ mô tả tốt.
Lớp Applicativekiểu nằm ở đâu đó giữa Functorvà Monad, vì vậy người ta sẽ mong đợi nó có cơ sở toán học tương tự. Tài liệu cho Control.Applicativemô-đun bắt đầu bằng:
Mô-đun này mô tả một cấu trúc trung gian giữa một bộ chức năng và một đơn nguyên: nó cung cấp các biểu thức và giải trình tự thuần túy, nhưng không có ràng buộc. (Về mặt kỹ thuật, một đầu dò monoidal lỏng lẻo mạnh mẽ.)
Hừ!
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
MonadTôi nghĩ không hoàn toàn hấp dẫn như vậy.
Về cơ bản, tất cả những điều này tóm lại là Applicativekhông tương ứng với bất kỳ khái niệm nào đặc biệt thú vị về mặt toán học, vì vậy không có thuật ngữ sẵn sàng nào xoay quanh cách nó được sử dụng trong Haskell. Vì vậy, hãy đặt toán học sang một bên ngay bây giờ.
Nếu chúng ta muốn biết (<*>)nó được gọi là gì, nó có thể hữu ích để biết nó về cơ bản nghĩa là gì.
Vì vậy, có chuyện gì thế với Applicative, dù sao, và tại sao làm chúng ta gọi nó vậy?
Có gì Applicativesố tiền để trong thực tế là một cách để nâng tùy ý chức năng vào một Functor. Hãy xem xét sự kết hợp của Maybe(được cho là kiểu dữ liệu không tầm thường đơn giản nhất Functor) và Bool(tương tự như vậy là kiểu dữ liệu không tầm thường đơn giản nhất).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Chức năng này fmapcho phép chúng tôi nâng cấp nottừ làm việc Boolsang làm việc Maybe Bool. Nhưng nếu chúng ta muốn nâng (&&)thì sao?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Chà, đó không phải là điều chúng ta muốn chút nào ! Trên thực tế, nó khá vô dụng. Chúng ta có thể cố gắng khéo léo và lẻn Boolvào một người khác Maybetừ phía sau ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... nhưng điều đó không tốt. Có điều, nó sai. Đối với một điều khác, nó xấu xí . Chúng tôi có thể tiếp tục thử, nhưng hóa ra là không có cách nào để nâng một hàm gồm nhiều đối số hoạt động trên một hàm tùy ýFunctor . Làm phiền!
Mặt khác, chúng ta có thể làm điều đó một cách dễ dàng nếu chúng ta sử dụng Maybe's Monaddụ:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Bây giờ, đó là rất nhiều rắc rối chỉ để dịch một hàm đơn giản - đó là lý do tại sao Control.Monadcung cấp một hàm để thực hiện nó tự động , liftM2. 2 trong tên của nó đề cập đến thực tế là nó hoạt động trên các chức năng của chính xác hai đối số; các hàm tương tự tồn tại cho các hàm đối số 3, 4 và 5. Các hàm này tốt hơn , nhưng không hoàn hảo và việc chỉ định số lượng đối số là xấu và vụng về.
Điều này đưa chúng ta đến với bài báo đã giới thiệu lớp Loại ứng dụng . Trong đó, các tác giả đưa ra hai nhận xét về cơ bản:
- Nâng các hàm đa đối số thành một
Functorlà một việc rất tự nhiên phải làm
- Làm như vậy không yêu cầu toàn bộ khả năng của
Monad
Ứng dụng chức năng bình thường được viết bằng cách ghép nối các thuật ngữ đơn giản, vì vậy, để làm cho "ứng dụng được nâng cấp" trở nên đơn giản và tự nhiên nhất có thể, bài báo giới thiệu các toán tử infix để ứng dụng, được nâng lênFunctor và một lớp loại để cung cấp những gì cần thiết cho điều đó .
Tất cả những điều đó đưa chúng ta đến điểm sau: (<*>)chỉ đơn giản là đại diện cho ứng dụng hàm - vậy tại sao lại phát âm nó khác với việc bạn sử dụng "toán tử ghép nối" khoảng trắng?
Nhưng nếu điều đó không hài lòng lắm, chúng ta có thể thấy rằng Control.Monadmô-đun cũng cung cấp một chức năng thực hiện điều tương tự cho các monads:
ap :: (Monad m) => m (a -> b) -> m a -> m b
apTất nhiên, ở đâu là viết tắt của "apply". Vì bất kỳ cái nào Monadcũng có thể có Applicativevà apchỉ cần tập hợp con của các tính năng có trong cái sau, chúng ta có thể nói rằng nếu (<*>)không phải là một toán tử, thì nó nên được gọi ap.
Chúng ta cũng có thể tiếp cận mọi thứ từ hướng khác. Các Functorhoạt động nâng được gọi là fmapbởi vì nó là một sự tổng quát của maphoạt động trên danh sách. Loại chức năng nào trên danh sách sẽ hoạt động như thế (<*>)nào? Tất nhiên, có những gì aptrong danh sách, nhưng bản thân nó không đặc biệt hữu ích.
Trên thực tế, có một cách giải thích có lẽ tự nhiên hơn cho các danh sách. Bạn nghĩ đến điều gì khi nhìn vào kiểu chữ ký sau đây?
listApply :: [a -> b] -> [a] -> [b]
Có điều gì đó rất hấp dẫn về ý tưởng xếp các danh sách song song, áp dụng từng chức năng trong phần tử đầu tiên cho phần tử tương ứng của phần tử thứ hai. Thật không may cho người bạn cũ của chúng tôi Monad, thao tác đơn giản này vi phạm luật đơn nguyên nếu danh sách có độ dài khác nhau. Nhưng nó có ích Applicative, trong trường hợp đó (<*>)trở thành một cách xâu chuỗi lại với nhau một phiên bản tổng quát của zipWith, vì vậy có lẽ chúng ta có thể hình dung cách gọi nó fzipWith?
Ý tưởng nén này thực sự mang lại cho chúng ta vòng tròn đầy đủ. Nhớ lại công cụ toán học trước đó, về các bộ chức năng đơn tử? Như tên cho thấy, đây là một cách kết hợp cấu trúc của monoids và functors, cả hai đều là các lớp kiểu Haskell quen thuộc:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Những thứ này sẽ trông như thế nào nếu bạn đặt chúng vào một chiếc hộp và lắc nó lên một chút? Từ Functorchúng tôi sẽ giữ ý tưởng về một cấu trúc độc lập với tham số kiểu của nó và từ Monoidchúng tôi sẽ giữ nguyên dạng tổng thể của các hàm:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Chúng tôi không muốn giả định rằng có một cách để tạo ra Functormột giá trị "trống" thực sự và chúng tôi không thể gợi ra một giá trị của một kiểu tùy ý, vì vậy chúng tôi sẽ sửa kiểu mfEmptylà f ().
Chúng tôi cũng không muốn bắt buộc mfAppendphải cần một tham số kiểu nhất quán, vì vậy bây giờ chúng tôi có điều này:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Loại kết quả để làm mfAppendgì? Chúng tôi có hai loại tùy ý mà chúng tôi không biết gì về nó, vì vậy chúng tôi không có nhiều lựa chọn. Điều hợp lý nhất là chỉ cần giữ cả hai:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
Tại thời điểm mfAppendnày rõ ràng là một phiên bản tổng quát của zipdanh sách và chúng tôi có thể Applicativedễ dàng tạo lại :
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Điều này cũng cho chúng ta thấy rằng purecó liên quan đến phần tử nhận dạng của a Monoid, vì vậy các tên hay khác cho nó có thể là bất kỳ thứ gì gợi ý giá trị đơn vị, phép toán null hoặc tương tự.
Điều đó dài dòng, vì vậy để tóm tắt:
(<*>) chỉ là một ứng dụng chức năng đã được sửa đổi, vì vậy bạn có thể đọc nó là "ap" hoặc "áp dụng", hoặc giải thích hoàn toàn theo cách bạn làm với ứng dụng chức năng bình thường.
(<*>)cũng khái quát zipWithvề danh sách, vì vậy bạn có thể đọc nó là "zip functors with", tương tự như đọc fmaplà "map a functor với".
Đầu tiên là gần với mục đích của Applicativelớp kiểu - như tên cho thấy - vì vậy đó là những gì tôi đề xuất.
Trên thực tế, tôi khuyến khích sử dụng tự do và không phát âm, của tất cả các toán tử ứng dụng đã nâng :
(<$>), nâng một hàm đối số đơn thành một Functor
(<*>), chuỗi này chuỗi một hàm đa đối số thông qua một Applicative
(=<<), liên kết một hàm nhập a Monadvào một phép tính hiện có
Về cơ bản, cả ba đều chỉ là ứng dụng chức năng thông thường, được gia vị thêm một chút.