Рассмотрим 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
операции, которые производят другой тип результата, становится намного, намного сложнее давать такие гарантии.