Hãy xem xét Functor
lớp kiểu trong Haskell, đâu f
là biến kiểu kiểu cao hơn:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Chữ ký kiểu này nói gì là fmap thay đổi tham số kiểu của một f
từ a
thành b
, nhưng vẫn f
giữ nguyên như cũ. Vì vậy, nếu bạn sử dụng fmap
trên một danh sách, bạn sẽ nhận được một danh sách, nếu bạn sử dụng nó trên một trình phân tích cú pháp, bạn sẽ nhận được một trình phân tích cú pháp, v.v. Và đây là những đảm bảo tĩnh , thời gian biên dịch.
Tôi không biết F #, nhưng chúng ta hãy xem xét điều gì sẽ xảy ra nếu chúng ta cố gắng diễn đạt sự Functor
trừu tượng bằng một ngôn ngữ như Java hoặc C #, với sự kế thừa và generics, nhưng không có generic cao hơn. Lần thử đầu tiên:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Vấn đề với lần thử đầu tiên này là việc triển khai giao diện được phép trả về bất kỳ lớp nào thực hiện Functor
. Ai đó có thể viết một phương thức FunnyList<A> implements Functor<A>
có map
phương thức trả về một loại tập hợp khác hoặc thậm chí một cái gì đó khác không phải là một tập hợp nhưng vẫn là một Functor
. Ngoài ra, khi bạn sử dụng map
phương thức này, bạn không thể gọi bất kỳ phương thức cụ thể kiểu phụ nào trên kết quả trừ khi bạn chuyển nó xuống kiểu mà bạn thực sự mong đợi. Vì vậy, chúng tôi có hai vấn đề:
- Hệ thống kiểu không cho phép chúng ta thể hiện bất biến rằng
map
phương thức luôn trả về cùng một Functor
lớp con với người nhận.
- Do đó, không có cách an toàn kiểu tĩnh nào để gọi một
Functor
phương thức không phải là kết quả của map
.
Bạn có thể thử những cách khác phức tạp hơn, nhưng không có cách nào thực sự hiệu quả. Ví dụ: bạn có thể thử tăng cường lần thử đầu tiên bằng cách xác định các kiểu phụ của kiểu Functor
hạn chế kết quả:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Điều này giúp cấm những người triển khai các giao diện hẹp hơn đó trả về loại sai Functor
từ map
phương thức, nhưng vì không có giới hạn về số lượng Functor
triển khai bạn có thể có, nên không có giới hạn về số lượng giao diện hẹp hơn bạn sẽ cần.
( CHỈNH SỬA: Và lưu ý rằng điều này chỉ hoạt động vì nó Functor<B>
xuất hiện dưới dạng loại kết quả và do đó, giao diện con có thể thu hẹp nó. Vì vậy AFAIK chúng tôi không thể thu hẹp cả hai cách sử dụng Monad<B>
trong giao diện sau:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Trong Haskell, với các biến kiểu cấp cao hơn, đây là (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
Tuy nhiên, một thử nghiệm khác là sử dụng các generic đệ quy để thử và có giao diện hạn chế kiểu kết quả của kiểu con đối với chính kiểu con đó. Ví dụ đồ chơi:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Nhưng loại kỹ thuật này (khá phức tạp đối với nhà phát triển OOP hàng đầu của bạn, còn đối với nhà phát triển chức năng hàng loạt của bạn) vẫn không thể thể hiện Functor
ràng buộc mong muốn :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Vấn đề ở đây là điều này không hạn chế FB
có giống F
như FA
— vì vậy khi bạn khai báo một kiểu List<A> implements Functor<List<A>, A>
, map
phương thức vẫn có thể trả về a NotAList<B> implements Functor<NotAList<B>, B>
.
Lần thử cuối cùng, trong Java, sử dụng các kiểu thô (vùng chứa chưa được phân loại):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Ở đây F
sẽ được khởi tạo cho các loại chưa được phân loại như just List
hoặc Map
. Điều này đảm bảo rằng a FunctorStrategy<List>
chỉ có thể trả về a List
—nhưng bạn đã từ bỏ việc sử dụng các biến kiểu để theo dõi các loại phần tử của danh sách.
Trọng tâm của vấn đề ở đây là các ngôn ngữ như Java và C # không cho phép các tham số kiểu có tham số. Trong Java, nếu T
là một biến kiểu, bạn có thể viết T
và List<T>
nhưng không T<String>
. Các loại thân thiện hơn loại bỏ hạn chế này, do đó bạn có thể có một cái gì đó như thế này (không được suy nghĩ đầy đủ):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
Và giải quyết vấn đề này cụ thể:
(Tôi nghĩ) Tôi hiểu rằng thay vì myList |> List.map f
hoặc myList |> Seq.map f |> Seq.toList
các loại kinded cao hơn cho phép bạn chỉ cần viết myList |> map f
và nó sẽ trả về a List
. Điều đó thật tuyệt (giả sử nó chính xác), nhưng có vẻ hơi nhỏ? (Và nó không thể được thực hiện đơn giản bằng cách cho phép quá tải hàm?) Tôi thường chuyển đổi thành Seq
anyway và sau đó tôi có thể chuyển đổi thành bất kỳ thứ gì tôi muốn sau đó.
Có nhiều ngôn ngữ khái quát hóa ý tưởng của map
hàm theo cách này, bằng cách mô hình hóa nó như thể, về cơ bản, ánh xạ là về các chuỗi. Nhận xét này của bạn là trên tinh thần đó: nếu bạn có một loại hỗ trợ chuyển đổi sang và đi Seq
, bạn sẽ nhận được hoạt động bản đồ "miễn phí" bằng cách sử dụng lại Seq.map
.
Tuy nhiên, trong Haskell, Functor
lớp này chung chung hơn thế; nó không bị ràng buộc với khái niệm về trình tự. Bạn có thể triển khai fmap
cho các loại không có ánh xạ tốt tới các chuỗi, như IO
hành động, tổ hợp phân tích cú pháp, hàm, v.v.:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
Khái niệm "ánh xạ" thực sự không gắn liền với trình tự. Tốt nhất bạn nên hiểu luật của functor:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Rất chính thức:
- Luật đầu tiên nói rằng ánh xạ với một hàm nhận dạng / noop giống như không làm gì cả.
- Định luật thứ hai nói rằng bất kỳ kết quả nào bạn có thể tạo ra bằng cách ánh xạ hai lần, bạn cũng có thể tạo ra bằng cách ánh xạ một lần.
Đây là lý do tại sao bạn muốn fmap
duy trì kiểu — bởi vì ngay sau khi bạn nhận được các map
phép toán tạo ra một kiểu kết quả khác, việc đảm bảo như thế này sẽ trở nên khó khăn hơn rất nhiều.