Зачистки конструктор типа


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

process (Pick xs) = ((First x) -> x) . mconcat . map (First . process) $ xs
Я думаю, что лямбда излишне многословна. Я хотел бы написать что-то вроде этого:
process (Pick xs) = -First . mconcat . map (First . process) $ xs

Позволяют ли средства метапрограммирования Haskell что-либо подобное этому? Любое другое решение для решения этой проблемы в более сжатом виде также приветствуется.

UPD. То весь код был запрошен:

data Node where
  Join :: [Node] -> Node
  Pick :: [Node] -> Node
  Given :: Maybe String -> Node
  Name :: String -> Node

process :: Node -> Maybe String
process (Join xs) = liftM os_path_join (mapM process xs)
process (Pick xs) = getFirst . mconcat . map (First . process) $ xs
process (Name x) = Just x
process (Given x) = x
4 7

4 ответа:

Как предложил Дзета в комментариях, coerce это хороший, общий способ сделать это:

process (Pick xs) = coerce . mconcat . map (First . process) $ xs

Еще одна приятная вещь о coerce заключается в том, что вы можете использовать его для принудительного использования" внутри " конструктора типа без затрат времени выполнения, например:

example :: [Sum Int] -> [Int]
example = coerce

Альтернатива, map getFirst, вызвала бы накладные расходы времени выполнения для обхода map.

Кроме того, каждый раз, когда вы создаете newtype, GHC автоматически создает соответствующий экземпляр Coercible, поэтому вам никогда не придется беспокоиться о том, чтобы возиться с базовый механизм (вам даже не нужно deriving для него):

newtype Test = Test Char

example2 :: Maybe Test -> Maybe Char
example2 = coerce

В этом случае вы можете использовать пакет newtypes для решения этой проблемы более обобщенно:

process :: Node -> Maybe String
process (Pick xs) = ala' First foldMap process xs
process (Join xs) = liftM os_path_join (mapM process xs)
process (Name x) = Just x
process (Given x) = x

Вы можете даже иметь более общую версию, которая принимает Newtype n (Maybe String) как

process'
    :: (Newtype n (Maybe String), Monoid n)
    => (Maybe String -> n) -> Node -> Maybe String
process' wrapper (Pick xs) = ala' wrapper foldMap (process' wrapper) xs
process' wrapper (Join xs) = liftM os_path_join (mapM (process' wrapper) xs)
process' wrapper (Name x) = Just x
process' wrapper (Given x) = x

Затем

> let processFirst = process' First
> let processLast = process' Last
> let input = Pick [Given Nothing, Name "bar", Given (Just "foo"), Given Nothing]
> processFirst input
Just "bar"
> ProcessLast input
Just "foo"

В качестве объяснения того, как это работает, функция ala' принимает оболочку newtype, чтобы определить экземпляр Newtype для использования, функцию, которой в этом случае мы хотим быть foldMap:

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Поскольку foldMap f в конечном итоге является обобщенным mconcat . map f над Foldable типами вместо того, чтобы просто перечислять, затем функция, которую можно использовать в качестве "препроцессора" для подключения к переданной функции более высокого порядкаala' (foldMap), затем в этом случае некоторые Foldable t => t Node обработать. Если вам не нужен шаг предварительной обработки, вы просто используете ala, который использует id для своего препроцессора. Использование этой функции иногда может быть затруднено из-за ее сложного типа, но, как показывают примеры в документации, foldMap часто является хорошим выбором.

Сила этого в том, если бы вы захотели написать ваша собственная newtype обертка для Maybe String:

newtype FirstAsCaps = FirstAsCaps { getFirstAsCaps :: Maybe String }

firstAsCaps :: Maybe String -> FirstAsCaps
firstAsCaps = FirstAsCaps . fmap (fmap toUpper)

instance Monoid FirstAsCaps where
    mempty = firstAsCaps Nothing
    mappend (FirstAsCaps f) (FirstAsCaps g)
        = FirstAsCaps $ ala First (uncurry . on (<>)) (f, g)

instance Newtype FirstAsCaps (Maybe String) where
    pack = firstAsCaps
    unpack = getFirstAsCaps

Затем

> process' firstAsCaps input
Just "BAR"

Если вы используете Data.Monoid.First, то это просто getFirst. Многие обертки newtype используют синтаксис записи, чтобы обеспечить простую функцию для разворачивания newtype.

Мета-Программирование выглядит слишком сложным для этого. Я бы просто использовал

unFirst (First x) = x  -- define once, use many times

process (Pick xs) = unFirst . mconcat . map (First . process) $ xs

Часто бывает так, что функция определяется вместе с типом newtype, например

newtype First a = First { unFirst :: a }