Моноиды и 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 newtypes, поэтому все эти махинации будут выполняться компилятором и исчезнут во время выполнения.


существует (неявно, если не явно) моноидный экземпляр для обычных функций вида 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 -> aS поэтому они имеют их естественное Monoid экземпляра. После foldMap сделано уменьшение нашего складного путем поворачивать все в a -> as и связывая их вместе с составом, мы извлекаем окончательный a -> a используя appEndo и, наконец, применить его к 0.