Рассмотрим Functorкласс типа в Haskell, где f- переменная типа более высокого порядка:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Сигнатура этого типа говорит о том, что fmap изменяет параметр типа fот aдо b, но оставляет без изменений f. Итак, если вы используете fmapсписок, вы получаете список, если вы используете его через синтаксический анализатор, вы получаете синтаксический анализатор и так далее. И это статические гарантии времени компиляции.
Я не знаю F #, но давайте посмотрим, что произойдет, если мы попытаемся выразить Functorабстракцию на таком языке, как Java или C #, с наследованием и дженериками, но без дженериков более высокого порядка. Первая попытка:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Проблема с этой первой попыткой заключается в том, что реализация интерфейса может возвращать любой класс, который реализует Functor. Кто-то может написать, FunnyList<A> implements Functor<A>чей mapметод возвращает коллекцию другого типа, или даже что-то еще, что вообще не является коллекцией, но все же остается Functor. Кроме того, когда вы используете этот mapметод, вы не можете вызывать какие-либо методы, зависящие от подтипа, для результата, если вы не преобразуете его в тот тип, который вы действительно ожидаете. Итак, у нас есть две проблемы:
- Система типов не позволяет нам выразить инвариант, что
mapметод всегда возвращает тот же Functorподкласс, что и получатель.
- Следовательно, не существует статически безопасного способа вызова не-
Functorметода для результата map.
Вы можете попробовать и другие, более сложные способы, но ни один из них не работает. Например, вы можете попробовать расширить первую попытку, определив подтипы, Functorкоторые ограничивают тип результата:
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);
}
Это помогает запретить разработчикам этих более узких интерфейсов возвращать Functorиз mapметода неправильный тип , но, поскольку нет ограничений на количество Functorреализаций, которые вы можете иметь, нет ограничений на то, сколько более узких интерфейсов вам понадобится.
( EDIT: и обратите внимание, что это работает только потому, что Functor<B>отображается как тип результата, и поэтому дочерние интерфейсы могут сузить его. Итак, AFAIK мы не можем сузить оба использования Monad<B>в следующем интерфейсе:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
В Haskell с переменными типа более высокого ранга это так (>>=) :: Monad m => m a -> (a -> m b) -> m b.)
Еще одна попытка - использовать рекурсивные универсальные шаблоны, чтобы попытаться ограничить интерфейс типом результата подтипа самим подтипом. Пример игрушки:
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);
}
Но этот вид техники (который довольно загадочен для вашего обычного разработчика ООП, черт возьми, также для вашего обычного функционального разработчика) все еще не может выразить желаемое Functorограничение:
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Проблема здесь в том, что это не ограничивает FBиспользование того же, Fчто и FA- поэтому, когда вы объявляете тип List<A> implements Functor<List<A>, A>, mapметод все равно может возвращать NotAList<B> implements Functor<NotAList<B>, B>.
Последняя попытка в Java с использованием необработанных типов (непараметризованные контейнеры):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Здесь Fбудут созданы экземпляры непараметризованных типов, таких как просто Listили Map. Это гарантирует, что a FunctorStrategy<List>может возвращать только a, Listно вы отказались от использования переменных типа для отслеживания типов элементов списков.
Суть проблемы здесь в том, что такие языки, как Java и C #, не позволяют параметрам типа иметь параметры. В Java, если Tэто переменная типа, вы можете написать Tи List<T>, но не T<String>. Высокородные типы снимают это ограничение, чтобы у вас могло быть что-то вроде этого (не до конца продуманное):
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) {
}
}
И, в частности, обращаясь к этому биту:
(Я думаю) я понимаю, что вместо myList |> List.map fили myList |> Seq.map f |> Seq.toListболее высокодородных типов вы можете просто писать, myList |> map fи он вернет List. Это здорово (при условии, что это правильно), но кажется мелочным? (И нельзя ли это сделать, просто разрешив перегрузку функций?) Я обычно Seqвсе равно конвертирую, а потом могу конвертировать во все, что захочу.
Есть много языков, которые таким образом обобщают идею mapфункции, моделируя ее так, как если бы, по сути, отображение связано с последовательностями. Это ваше замечание в том же духе: если у вас есть тип, поддерживающий преобразование в и из Seq, вы получаете операцию карты «бесплатно», повторно используя Seq.map.
В Haskell, однако, этот Functorкласс является более общим; это не связано с понятием последовательностей. Вы можете реализовать fmapтипы, которые не имеют хорошего сопоставления с последовательностями, например IOдействия, комбинаторы синтаксического анализатора, функции и т. Д .:
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)
Концепция «отображения» на самом деле не привязана к последовательностям. Лучше всего понять законы функторов:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Очень неформально:
- Первый закон гласит, что отображение с функцией identity / noop - это то же самое, что и бездействие.
- Второй закон гласит, что любой результат, который вы можете получить, нанеся двойное отображение, вы также можете получить однократным отображением.
Вот почему вы хотите fmapсохранить тип - потому что, как только вы получаете mapоперации, которые производят другой тип результата, становится намного, намного сложнее давать такие гарантии.