Розглянемо 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);
}
Але такий метод (який є досить загадковим для вашого розробника OOP, а також для вашого функціонального розробника) ще не може виразити бажане 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метод все одно може повернути a NotAList<B> implements Functor<NotAList<B>, B>.
Останній спробу на Java, використовуючи необроблені типи (непараметризовані контейнери):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Тут Fбуде отримати примірник для unparametrized типів , як тільки Listабо Map. Це гарантує, що a FunctorStrategy<List>може повернути лише aList - але ви відмовились від використання змінних типу для відстеження типів елементів у списках.
Суть проблеми полягає в тому, що такі мови, як 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і це поверне a List. Це чудово (якщо припустити, що це правильно), але здається якийсь дріб’язковий? (І чи не можна це зробити, просто дозволивши перевантаження функції?) Я зазвичай перетворюю на Seqвсе одно, а потім згодом можу перетворити на все, що хочу.
Є багато мов, які узагальнюють ідею mapфункції таким чином, моделюючи її так, ніби в основі суть відображення - це послідовності. Це ваше зауваження в цьому дусі: якщо у вас є тип, який підтримує перетворення в і з Seq, ви отримуєте операцію з картою "безкоштовно", повторно використовуючи Seq.map.
Однак у Хаскелі 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
Дуже неофіційно:
- Перший закон говорить, що відображення за допомогою функції ідентичності / noop - це те саме, що нічого не робити.
- Другий закон говорить, що будь-який результат, який ви можете отримати шляхом картографування двічі, ви також можете отримати, виконавши картографування один раз.
Ось чому ви хочете fmapзберегти тип - адже як тільки ви отримуєте mapоперації, які дають інший тип результату, стає набагато важче зробити такі гарантії.