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 Applicative
kiểu nằm ở đâu đó giữa Functor
và 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.Applicative
mô-đ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
. . .
Monad
Tô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à Applicative
khô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ì Applicative
số 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 fmap
cho phép chúng tôi nâng cấp not
từ làm việc Bool
sang 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 Bool
vào một người khác Maybe
từ 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 Monad
dụ:
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.Monad
cung 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
Functor
là 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.Monad
mô-đ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
ap
Tất nhiên, ở đâu là viết tắt của "apply". Vì bất kỳ cái nào Monad
cũng có thể có Applicative
và ap
chỉ 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 Functor
hoạt động nâng được gọi là fmap
bởi vì nó là một sự tổng quát của map
hoạ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ì ap
trong 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ừ Functor
chú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ừ Monoid
chú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 Functor
mộ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 mfEmpty
là f ()
.
Chúng tôi cũng không muốn bắt buộc mfAppend
phả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 mfAppend
gì? 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 mfAppend
này rõ ràng là một phiên bản tổng quát của zip
danh sách và chúng tôi có thể Applicative
dễ 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 pure
có 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 zipWith
về danh sách, vì vậy bạn có thể đọc nó là "zip functors with", tương tự như đọc fmap
là "map a functor với".
Đầu tiên là gần với mục đích của Applicative
lớ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 Monad
và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.