Моноиды и Num в Haskell
Я изучал Хаскелла в течение последних нескольких месяцев, и я наткнулся на пример моноидов, который меня озадачил.
учитывая эти определения:
data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
instance F.Foldable Tree where
foldMap f Empty = mempty
foldMap f (Node x l r) = F.foldMap f l `mappend`
f x `mappend`
F.foldMap f r
и это дерево:
testTree = Node 5
(Node 3
(Node 1 Empty Empty)
(Node 6 Empty Empty)
)
(Node 9
(Node 8 Empty Empty)
(Node 10 Empty Empty)
)
Если я запускаю:
ghci> F.foldl (+) 0 testTree
42
ghci> F.foldl (*) 1 testTree
64800
как GHCi знает, какой моноид использовать для отображения, когда он складывается? Потому что по умолчанию числа в дереве имеют тип Num, и мы никогда явно не говорили, что они имеют какой-то моноид, такой как Sum или Товар.
Итак, как GHCi выводит правильный моноид для использования? Или я совсем не в себе?
пример источника: http://learnyouahaskell.com/functors-applicative-functors-and-monoids#monoids
3 ответов
короткий ответ:: это ограничение типа в сигнатуре foldMap
.
если мы посмотрим на исходный код Foldable
(более конкретно foldMap
), мы видим:
class Foldable (t :: * -> *) where
...
foldMap :: Monoid m => (a -> m) -> t a -> m
значит, если мы объявим Tree
член Foldable
(а не Tree
имеет вид * -> *
), это означает, что a foldMap
определено, что дерево, такое, что: foldMap :: Monoid m => (a -> m) -> Tree a -> m
. Таким образом это означает, что результат (и результат функции перешел в foldMap
) m
должен быть Monoid
.
Haskell статически типизирован: после времени компиляции Haskell точно знает типы, которые передаются в out каждой функции экземпляр. Таким образом, это означает, что он знает, например, какой тип вывода будет, и, следовательно, как его обрабатывать.
теперь Int
не является экземпляром Monoid
. Вы здесь используете F.foldl (+) 0 testTree
, так что это означает, что вы более или менее построили "специальный" моноид. Это работает, если мы посмотри код foldl
:
foldl :: (b -> a -> b) -> b -> t a -> b foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z
это имеет много логики, окружающей параметры f
, z
и t
. Давайте сначала разберем это.
давайте сначала посмотрим на Dual . Endo . flip f
. Это сокращение от:
helper = \x -> Dual (Endo (\y -> f y x))
Dual
и Endo
являются типами с каждым конструктором, который принимает один параметр. Итак, мы завершаем результат f y x
на Dual (Endo ...)
проектировщики.
мы будем использовать это как функцию foldMap
. Если наши f
типа a -> b -> a
, то эта функция имеет тип b -> Dual (Endo a)
. Таким образом, тип вывода функции передается в foldMap
имеет выход типа Dual (Endo a)
. Теперь, если мы проверим исходный код, мы увидим две интересные вещи:
instance Monoid (Endo a) where mempty = Endo id Endo f `mappend` Endo g = Endo (f . g) instance Monoid a => Monoid (Dual a) where mempty = Dual mempty Dual x `mappend` Dual y = Dual (y `mappend` x)
(отметим, что это y `mappend` x
, а не x `mappend` y
).
так что здесь происходит то, что mempty
который используется в foldMap
is mempty = Dual mempty = Dual (Endo id)
. Так Dual (Endo ...)
, который инкапсулирует функция удостоверение.
кроме того,mappend
из двух дуалов сводится к состав функции значения Endo
. Итак:
mempty = Dual (Endo id)
mappend (Dual (Endo f)) (Dual (Endo g)) = Dual (Endo (g . f))
так что это означает, что если мы складываем над деревом, в случае, если мы видим Empty
(лист), мы вернемся mempty
, и в случае мы видим Node x l r
, мы выполним mappend
как описано выше. Так что"специализированный" foldMap
будет выглядеть так:
-- specialized foldMap
foldMap f Empty = Dual (Endo id)
foldMap f (Node x l r) = Dual (Endo (c . b . a))
where Dual (Endo a) = foldMap f l
Dual (Endo b) = helper x
Dual (Endo c) = foldMap f l
для всех Node
мы делаем функциональную композицию справа налево над дочерними элементами и элементом узла. a
и c
также могут быть составами дерева (поскольку это рекурсивные вызовы). В случае Leaf
, мы ничего не делать (мы возвращаемся id
, но композиция закончилась id
нет).
значит, если у нас есть дерево:
5
|- 3
| |- 1
| `- 6
`- 9
|- 8
`- 10
это приведет к функция:
(Dual (Endo ( (\x -> f x 10) .
(\x -> f x 9) .
(\x -> f x 8) .
(\x -> f x 5) .
(\x -> f x 6) .
(\x -> f x 3) .
(\x -> f x 1)
)
)
)
(опущено личности, чтобы сделать его чище). Это результат getDual (foldMap (Dual . Endo . flip f))
. Но теперь нам нужно опубликовать этот результат. С getDual
, мы получаем содержимое, завернутое в Dual
конструктор. Итак, теперь у нас есть:
Endo ( (\x -> f x 10) .
(\x -> f x 9) .
(\x -> f x 8) .
(\x -> f x 5) .
(\x -> f x 6) .
(\x -> f x 3) .
(\x -> f x 1)
)
и appEndo
, мы получаем функцию, обернутую в Endo
, так:
( (\x -> f x 10) .
(\x -> f x 9) .
(\x -> f x 8) .
(\x -> f x 5) .
(\x -> f x 6) .
(\x -> f x 3) .
(\x -> f x 1)
)
и затем мы применяем это к z
"первоначальной" стоимости. Это значит, что мы будем обрабатывать цепочку, начиная с z
(начальный элемент) и примените его так:
f (f (f (f (f (f (f z 1) 3) 6) 5) 8) 9) 10
Итак, мы построили какой-то моноид, где mappend
заменить на f
и mempty
как no-op (функция идентификации).
в этом нет необходимости. foldl
переведена на foldr
что означает foldMap
над Endo
что означает композицию функции, которая означает простую вложенность функции вы поставляли.
или что-то в этом роде. Значит,foldl
может быть переведена на foldMap
над Dual . Endo
который пишет слева-направо, и т. д..
обновление: да врачи говорят:
складные экземпляры ожидается, чтобы удовлетворить следующие законы:
foldr f z t = appEndo (foldMap (Endo . f) t ) z foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z -- << -- fold = foldMap id
Dual (Endo f) <> Dual (Endo g) = Dual (Endo g <> Endo f) = Dual (Endo (g . f))
. Так когда appEndo
strikes, цепочка функций, которая была построена, т. е.
((+10) . (+9) . (+8) . (+5) . ... . (+1))
или эквивалент (здесь показано для (+)
case), применяется к пользовательскому значению -- в вашем случае,
0
еще одна вещь, чтобы заметить, что Endo
и Dual
are newtype
s, поэтому все эти махинации будут выполняться компилятором и исчезнут во время выполнения.
существует (неявно, если не явно) моноидный экземпляр для обычных функций вида a -> a
, где mappend
соответствует функции, состав, и mempty
соответствует над вашим складным полным чисел с +
, вы превращаете каждый из них в частично примененный (+ <some number)
, который является a -> a
. Смотрите, вы нашли волшебство!--12--> что получится все в вашем складном в моноид!
предполагая, что существует прямой экземпляр моноида для функций, вы сможете сделать:
foldMap (+) [1, 2, 3, 4]
, который произвел бы окончательный (Num a) => a -> a
что вы могли бы обратиться к 0
и 10
.
однако нет такого прямого экземпляра, поэтому вам нужно использовать встроенный newtype
фантик Endo
и соответствующие unwrapper appEndo
, которые реализуют моноид для a -> a
функции. Вот как это выглядит например:
Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0
10
здесь Endo .
это просто наша раздражающая необходимость поднять равнину a -> a
S поэтому они имеют их естественное Monoid
экземпляра. После foldMap
сделано уменьшение нашего складного путем поворачивать все в a -> a
s и связывая их вместе с составом, мы извлекаем окончательный a -> a
используя appEndo
и, наконец, применить его к 0
.