Подтвердить что ты не робот

Какова практическая ценность всех этих оболочек newtype в "Data.Monoid"?

При просмотре Data.Monoid я вижу, что существуют различные обертки newtype, такие как All, Sum или Product, которые кодируют различные типы моноидов. Однако, пытаясь использовать эти обертки, я не могу не задаться вопросом, что польза от использования их не Data.Monoid. Например, сравните довольно громоздкое суммирование

print $ getSum $ mconcat [ Sum 33, Sum 2, Sum 55 ]

против. более сжатый идиоматический вариант

print $ sum [ 33, 2, 55 ]

Но какой смысл? Есть ли практическое значение, имеющее все эти обертки newtype? Есть ли более убедительные примеры использования оболочки Monoid newtype, чем приведенное выше?

4b9b3361

Ответ 1

Моноиды newtypes: нулевое пространство no-op, чтобы сообщить компилятору, что делать

Моноиды отлично переносят существующий тип данных в новый тип, чтобы сообщить компилятору, какую операцию вы хотите сделать.

Поскольку они являются новыми, они не занимают дополнительного места, а применение Sum или getSum - это не-op.

Пример: Моноиды в Складном

Здесь существует более чем один способ обобщить foldr (см. этот очень хороший вопрос для самой общей сводки, и этот вопрос, если вам нравятся приведенные ниже примеры деревьев, но вы хотите увидеть наиболее общую складку деревьев).

Один полезный способ (не самый общий способ, но, безусловно, полезный) - сказать что-то складное, если вы можете объединить его элементы в один с двоичной операцией и элементом start/identity. Это точка класса Foldable.

Вместо явного перехода в двоичную операцию и начальный элемент Foldable просто запрашивает, что тип данных элемента является экземпляром Monoid.

На первый взгляд это кажется разочаровывающим, потому что мы можем использовать только одну двоичную операцию для каждого типа данных - но следует ли использовать (+) и 0 для Int и принимать суммы, но никогда не продукты, или наоборот? Возможно, следует использовать ((+),0) для Int и (*),1 для Integer и конвертировать, когда мы хотим выполнить другую операцию? Разве это не будет тратить много драгоценных процессорных циклов?

Моноиды на помощь

Все, что нам нужно сделать, это тег Sum, если мы хотим добавить, пометить тегом Product, если мы хотим размножить, или даже пометить ручным новым типом, если мы хотим сделать что-то другое.

Сложим несколько деревьев! Нам понадобится

fold :: (Foldable t, Monoid m) => t m -> m    
   -- if the element type is already a monoid
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
   -- if you need to map a function onto the elements first

Расширения DeriveFunctor и DeriveFoldable ({-# LANGUAGE DeriveFunctor, DeriveFoldable #-}) великолепны, если вы хотите перевернуть и свернуть свой собственный ADT, не записывая утомительные экземпляры самостоятельно.

import Data.Monoid
import Data.Foldable
import Data.Tree
import Data.Tree.Pretty -- from the pretty-tree package

see :: Show a => Tree a -> IO ()
see = putStrLn.drawVerticalTree.fmap show

numTree :: Num a => Tree a
numTree = Node 3 [Node 2 [],Node 5 [Node 2 [],Node 1 []],Node 10 []]

familyTree = Node " Grandmama " [Node " Uncle Fester " [Node " Cousin It " []],
                               Node " Gomez - Morticia " [Node " Wednesday " [],
                                                        Node " Pugsley " []]]

Пример использования

Строки уже являются моноидами, использующими (++) и [], поэтому мы можем с ним fold, но чисел нет, поэтому мы будем отмечать их с помощью foldMap.

ghci> see familyTree
               " Grandmama "                
                     |                      
        ----------------------              
       /                      \             
" Uncle Fester "     " Gomez - Morticia "   
       |                      |             
 " Cousin It "           -------------      
                        /             \     
                  " Wednesday "  " Pugsley "
ghci> fold familyTree
" Grandmama  Uncle Fester  Cousin It  Gomez - Morticia  Wednesday  Pugsley "
ghci> see numTree       
     3                  
     |                   
 --------               
/   |    \              
2   5    10             
    |                   
    --                  
   /  \                 
   2  1                 

ghci> getSum $ foldMap Sum numTree
23
ghci> getProduct $ foldMap Product numTree
600
ghci> getAll $ foldMap (All.(<= 10)) numTree
True
ghci> getAny $ foldMap (Any.(> 50)) numTree
False

Сверните свой собственный моноид

Но что, если мы хотим найти максимальный элемент? Мы можем определить наши собственные моноиды. Я не уверен, почему MaxMin) не включены. Возможно, это потому, что никто не любит думать о Int, ограниченном или просто не нравится элемент идентификации, основанный на детализации реализации, В любом случае это:

newtype Max a = Max {getMax :: a}

instance (Ord a,Bounded a) => Monoid (Max a) where
   mempty = Max minBound
   mappend (Max a) (Max b) = Max $ if a >= b then a else b
ghci> getMax $ foldMap Max numTree :: Int  -- Int to get Bounded instance
10

Заключение

Мы можем использовать newtype Monoid wrappers, чтобы сообщить компилятору, каким образом совместить вещи в парах.

Теги ничего не делают, но показывают, какую комбинацию использовать.

Это похоже на передачу функций в виде неявного параметра, а не явного (потому что такой тип класса классов вообще).

Ответ 2

Как в таком экземпляре:

myData :: [(Sum Integer, Product Double)]
myData = zip (map Sum [1..100]) (map Product [0.01,0.02..])

main = print $ mconcat myData

Или без оболочки newtype и экземпляра Monoid:

myData :: [(Integer, Double)]
myData = zip [1..100] [0.01,0.02..]

main = print $ foldr (\(i, d) (accI, accD) -> (i + accI, d * accD)) (0, 1) myData

Это связано с тем, что (Monoid a, Monoid b) => Monoid (a, b). Теперь, что, если у вас были пользовательские типы данных, и вы хотели сбросить кортеж этих значений, применяя двоичную операцию? Вы могли бы просто написать оболочку newtype и сделать ее экземпляром Monoid с этой операцией, построить свой список кортежей, а затем просто использовать mconcat, чтобы свернуть их. Есть много других функций, которые работают и на Monoid, а не только на mconcat, поэтому, конечно, есть множество приложений.


Вы также можете посмотреть обертки First и Last newtype для Maybe a, я могу подумать о многих применениях для них. Оболочка Endo хороша, если вам нужно составить множество функций, обертки Any и All хороши для работы с булевыми элементами.

Ответ 3

Предположим, что вы работаете в монаде Writer, и хотите сохранить сумму всего, что вы tell. В этом случае вам понадобится обертка newtype.

Вам также понадобится newtype для использования таких функций, как foldMap, которые имеют ограничение Monoid.

Комбинаторы ala и alaf из Control.Lens.Wrapped в пакете lens могут сделать работу с этими новыми типами более приятной. Из документации:

>>> alaf Sum foldMap length ["hello","world"]
10

>>> ala Sum foldMap [1,2,3,4]
10

Ответ 4

Иногда вам просто нужно указать конкретный Monoid, чтобы заполнить ограничение типа. Иногда бывает, что Const имеет экземпляр Applicative, если он хранит Monoid.

instance Monoid m => Applicative (Const m) where
  pure _ = Const mempty
  Const a <*> Const b = Const (a <> b)

Это явно немного странно, но иногда это то, что вам нужно. Лучший пример, который я знаю, находится в lens, где вы заканчиваете такими типами, как

type Traversal s a = forall f . Applicative f => (a -> f a) -> (s -> f s)

Если вы специализируетесь f на что-то вроде Const First, используя Monoid newtype First

newtype First a = First { getFirst :: Maybe a }

-- Retains the first, leftmost 'Just'
instance Monoid (First a) where
  mempty = First Nothing
  mappend (First Nothing)  (First Nothing) = First Nothing
  mappend (First (Just x)) _               = First (Just x)

то мы можем интерпретировать этот тип

(a -> Const (First a) a) -> (s -> Const (First a) s)

как сканирование через s и подбирая первый a внутри него.


Итак, хотя это действительно конкретный ответ, широкая реакция заключается в том, что иногда полезно иметь возможность говорить о множестве разных типов поведения по умолчанию Monoid. В любом случае кому-то приходилось писать все очевидные поведения Monoid, и их также можно было бы поместить в Data.Monoid.

Ответ 5

Основная идея, я думаю, состоит в том, что вы можете иметь что-то вроде

reduce = foldl (<>) mempty

и он будет работать для любого списка этих завернутых вещей.