Различное поведение аппликатора на кортежах и списках в Haskell


Например,

-- Num a => ([Char], a -> a) <*> ([Char], a)
> ("hello ",(*6)) <*> ("world",7)
("hello world",42)

-- Num a => [a -> a] <*> [a]
> [(*7),(*6)] <*> [6,7]
[42,49,36,42]

-- Num a => [[Char], a -> a] <*> [[Char], a]
> ["hello ",(*6)] <*> ["world",7]
<interactive>:17:2:
    Couldn't match expected type ‘[Char] -> [Char]’
                with actual type ‘[Char]’
    In the expression: "hello "
    In the first argument of ‘(<*>)’, namely ‘["hello ", (* 6)]’
    In the expression: ["hello ", (* 6)] <*> ["world", 7]

Для трех примеров, <*> показывает различное поведение. Что же происходит? Почему в третьем случае он ожидает [Char] -> [Char], а не [Char], как в первом случае. Более того, даже есть только [Char] в кортежах, <*> объединяет их вместе.

2 3

2 ответа:

Разница заключается в том, что списки однородны, а кортежи-нет: списки содержат только элементы одного типа, а кортежи-нет.

Даже не глядя на аппликативы, функторы уже показывают основное различие:

fmap succ [1,2,3]  ==> [2,3,4]
fmap succ ("a", 4) ==> ???
Было бы нелогично утверждать, что fmap применимо succ к "a". Происходит то, что только второй компонент подвергается воздействию:
fmap succ ("a", 4) ==> ("a", 5)

Действительно, посмотрите на примеры:

instance Functor [] where ...
instance Functor ((,) a) where ...

Обратите внимание на a тип. В экземпляре списка [] принимает только один параметр типа, и это тип, на который влияет fmap. В (,) мы имеем два параметра типа: один фиксирован (к a) и не изменяется при применении fmap - только второй.

Обратите внимание, что теоретически возможно допустить экземпляр Functor (,), когда оба аргумента типа вынуждены быть одинаковыми. Например,
instance Functor (\b -> (b,b)) where ...
Но Хаскелл этого не допускает. Если нужно, нужна обертка newtype:
newtype P b = P (b,b)
instance Functor P where
   fmap f (P (x,y)) = P (f x, f y)

Аппликатив - это любая комбинация типа данных и определений pure и <*>, которая удовлетворяет аппликативу . законы :

    [identity] pure id <*> v = v
 [composition] pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
[homomorphism] pure f <*> pure x = pure (f x)
 [interchange] u <*> pure y = pure ($ y) <*> u
Эти законы гарантируют, что <*> ведет себя очень похоже на приложение функции, но происходит в некотором "особом контексте", который зависит от приложения. Для случая Maybe контекст-это возможное отсутствие значения. Для кортежей контекст-это "моноидальные аннотации, сопровождающие каждый кортеж". значение".

pure и <*> могут делать очень разные вещи для разных типов данных, пока они уважают законы.

На самом деле, один и тот же тип данных может быть Применительным по-разному. Списки имеют прикладной экземпляр, в котором <*> "получает все комбинации", но также экземпляр, реализованный со вспомогательным ZipList newtype, где <*> сжимает списки вместе и pure создает бесконечный список.