Моноиды и Num в Хаскелле

Итак, я изучал Haskell за последние несколько месяцев, и я наткнулся на пример Monoids, который меня озадачил

Учитывая эти определения

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 знает, что Monoid может использовать для mappend при его складывании, потому что по умолчанию числа в дереве являются только классом Num, и мы никогда явно не говорили о том, где некоторые моноиды, такие как Sum или Product, So как ghci выводит правильный моноид? или я полностью ушел в этот момент

Пример источника: http://learnyouahaskell.com/functors-applicative-functors-and-monoids#monoids

Ответы

Ответ 1

Короткий ответ: это ограничение типа в сигнатуре 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 точно знает типы, которые передаются из каждого экземпляра функции. Таким образом, это означает, что он знает, например, тип вывода, и, следовательно, как его обрабатывать.

Теперь Int не является экземпляром Monoid. Вы здесь используете F.foldl (+) 0 testTree, так что это означает, что вы более или менее создали "ad hoc" моноид. Это работает, если мы посмотрим на исходный код 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, это 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, и в случае, если мы увидим a 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 мы создаем композицию функции справа налево над дочерними элементами и элементом node. a и c могут быть композициями дерева (так как это рекурсивные вызовы). В случае a Leaf мы ничего не делаем (мы возвращаем id, но композиция над id является no-op).

Таким образом, это означает, что если у нас есть дерево:

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 как нет-op (функция тождества).

Ответ 2

Этого не нужно. foldl переводится в foldr, который переводится в foldMap поверх Endo, что означает состав функций, что означает простое вложение функции, которую вы предоставили.

Или что-то. Значение foldl может быть переведено на foldMap поверх Dual . Endo, которое составляет слева направо и т.д.

update: yes, документы:

Складываемые экземпляры, как ожидается, удовлетворяют следующим законам:

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 ударяет, цепочка построенных функций, т.е.

    ((+10) . (+9) . (+8) . (+5) . ... . (+1))

или эквивалент (здесь, показанный для случая (+)), применяется к предоставленному пользователем значению - в вашем случае

                                                 0

Еще одна вещь, которую следует заметить, заключается в том, что Endo и Dual являются newtype s, поэтому все эти махинации будут выполняться компилятором и будут уходить по времени выполнения.

Ответ 3

Существует (неявно, если не явно) моноидный экземпляр для обычных функций вида a -> a, где mappend соответствует составу функции, а mempty соответствует функции id.

Что такое (+)? Это функция (Num a) => a -> a -> a. Если вы foldMap над вашим складным количеством чисел с помощью +, вы поворачиваете каждый в частично приложенный (+ <some number), который является a -> a. И вот, вы нашли волшебство f, которое превратит все в ваш складной в моноид!

Предполагая, что существует прямой моноидный экземпляр для функций, вы могли бы сделать:

foldMap (+) [1, 2, 3, 4]

что создаст конечный (Num a) => a -> a, который вы могли бы применить к 0, чтобы получить 10.

Однако такого прямого экземпляра нет, поэтому вам нужно использовать встроенную оболочку newtype Endo и соответствующий разворачиватель appEndo, которые реализуют функции monoid для a -> a. Вот как это выглядит:

Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0
10

Здесь Endo . просто наша досадная необходимость поднять гладкую a -> a, чтобы у них был естественный экземпляр Monoid. После выполнения foldMap, уменьшающего нашу складку, превращая все в a -> a и объединяя их вместе с композицией, мы извлекаем окончательный a -> a с помощью appEndo и, наконец, применяем его к 0.