2014-11-19 3 views
4

У меня есть функция, например, так:Многократное же тело функции для нескольких подписей типа

test :: [Block] -> [Block] 
test ((Para foobar):rest) = [Para [(foobar !! 0)]] ++ rest 
test ((Plain foobar):rest) = [Plain [(foobar !! 0)]] ++ rest 

Block является типом данных, который включает в себя Para, Plain и других. Что делает эта функция не особо важной, но я замечаю, что тело функции ([Para [(foobar !! 0)]] ++ rest) одинаково для обоих Para и Plain, за исключением того, что используемым конструктором является тип foobar.

Вопрос: Есть ли способ, чтобы кратко написать эту функцию с двумя комбинированными случаями? Нечто подобное

test :: [Block] -> [Block] 
test ((ParaOrPlain foobar):rest) = [ParaOrPlain [(foobar !! 0)]] ++ rest 

где первые ParaOrPlain матчи либо Para foobar или Plain foobar, а второй ParaOrPlain является Para или Plain соответственно.

Обратите внимание, что Block также может быть BulletList или OrderedList, и я не хочу работать с ними. (Редактирование:. test x = x для этих других типов)

Ключ в том, что я не хочу, чтобы повторить тело функции дважды, так как они идентичны (за исключением вызов Para или Plain).

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


Редактировать Чтобы уточнить, я знаю, что моя функция тело громоздкое (я новичок в Haskell), и я благодарю различные за их отвечающие упрощения.

Однако, в центре проблемы, я хочу избежать репликации линии Para и Plain. Нечто подобное (в моем madeup языке ...)

# assume we have `test (x:rest)` where `x` is an arbirtrary type 
if (class(x) == 'Para' or class(x) == 'Plain') { 
    conFun = Plain 
    if (class(x) == 'Para') 
     conFun = Para 
    return [conFun ...] 
} 
# else do nothing (ID function, test x = x) 
return x:rest 

т.е. я хочу знать, если это возможно в Haskell, чтобы назначить функцию конструктора в зависимости от типа входного параметра таким образом. Надеюсь, что это прояснит вопрос.

+0

Я ответил - и удалил этот ответ - ниже, предлагая экземпляр «Functor» для «Block», который немного озарен, поскольку «Блок» даже не подходит. – jgriego

+3

Обратите внимание, что ваш код составления более длинный и менее прозрачный, чем ваш код Haskell. Повторное использование кода - это хорошо, но ясность и последовательность лучше. – AndrewC

ответ

3

Один подход заключается в использовании (полиморфный) вспомогательные функции, например .:

helper ctor xs rest = ctor [ head xs ] : rest 

test :: [Block] -> [Block] 
test ((Para xs):rest) = helper Para xs rest 
test ((Plain xs):rest) = helper Plain xs rest 
test bs    = bs 

Заметьте, что Para и Plain просто функции типа [whatever] -> Block.

+0

Моя цель заключается в том, чтобы избежать репликации строк 'test (Para ..) = helper Para ...' и 'test (Plain ..) = helper Plain ...', так как они одинаковы, кроме Пара/Обычная замена. Наличие вспомогательной функции действительно уменьшает избыточность (спасибо!). Думаю, похоже, что это невозможно сделать в Haskell? –

+0

Это лучший баланс ближайшего к тому, чем я был, и то, что я могу понять (будучи новым для Haskell), и помогая мне избежать тиражирования. cheers –

1

Прежде всего обратите внимание, что, как уже писал пользователь user5402, вы должны заменить [Para [(foobar !! 0)]] ++ rest на Para [foobar !! 0] : rest, который выглядит уже неплохо. Далее заметим, что в принципе, все, что вы делаете это изменить главу списка, так как содержательного помощника я использовал бы

modifyHead :: (a->a) -> [a] -> [a] 
modifyHead f (x:xs) = f x : xs 
modifyHead _ [] = [] 

test = modifyHead m 
where m (Para foobar) = Para [head foobar] -- `head` is bad, but still better than `(!!0)`! 
     m (Plain foobar) = Plain [head foobar] 
+0

Спасибо за синтаксис с упрощениями (я совершенно новый для Haskell) !. Но основной вопрос заключается в том, что я хочу избежать дублирования строк 'm (Para foobar) и' m (Plain foobar) для удобства редактирования в будущем (они идентичны, за исключением типа). –

+0

Я вижу вашу точку зрения, но это мышление на самом деле не работает. «Идентичное сохранение для типа» не имеет смысла; если две вещи имеют разные типы, то они примерно так же различны, как и получают. Тем не менее, есть один трюк, который вы можете использовать ... – leftaroundabout

+0

... а именно, модификатор Rank2-generic. См. Мой другой ответ. – leftaroundabout

1

Это примерно так же хорошо, как я могу получить его без использования шаблона Haskell или данных ,Данные :-)

Прежде всего, мы должны включить некоторые расширения и зафиксировать Block тип данных для конкретности (если мои упрощающие предположения неверны, скажи мне, и я посмотрю, что можно спасти!)

{-# LANGUAGE GADTs, LambdaCase #-} 
data Block = Para [Int] 
      | Plain [String] 
      | Other Bool Block 
      | YetAnother deriving (Show,Eq) 

Важным моментом здесь является то, что Para и Plain являются унарными конструкторами, но сам тип данных может содержать конструкторы с различным числом аргументов.

Как пояснил @leftaroundabout и @ user5402, мы можем отделить проблемы изменения одного Block и применение этой модификации к первому элементу только список, поэтому я остановлюсь на переписывание

modifyBaseline :: Block -> Block 
modifyBaseline (Para xs) = Para [xs !! 0] 
modifyBaseline (Plain xs) = Plain [xs !! 0] 
modifyBaseline rest = rest 

Далее , мы должны иметь возможность говорить о унарных конструкторах как значениях. Есть 3 важных вещей здесь:

  • имеет значение соответствующего данный конструктор,
  • если да, то значения «внутри»,
  • и как мы можем (перо) построить такое значение.

Мы упаковываем это красиво в пользовательском типе (t тип конструктора принадлежит, и a является то, что внутри):

Итак, теперь мы можем определить

para :: UnaryConstructor Block [Int] 
para = UnaryConstructor (\case { Para ns -> Just ns ; _ -> Nothing }) Para 
plain :: UnaryConstructor Block [Int] 
plain = UnaryConstructor (\case { Plain ss -> Just ss ; _ -> Nothing }) Plain 

(Вы можете избавиться от расширения LambdaCase, если вы пишете (\xs -> case xs of { Para ns -> Just ns; _ -> Nothing}), но таким образом это приятно и компактно!)

Далее, нам нужно «упаковать» унарный конструктор и функцию применить к значению, содержащемуся в нем:

data UnaryConstructorModifier t where 
    UnaryConstructorModifier :: UnaryConstructor t a -> (a -> a) -> UnaryConstructorModifier t 

, так что мы можем написать

modifyHelper :: [UnaryConstructorModifier t] -> t -> t 
modifyHelper [] t = t 
modifyHelper ((UnaryConstructorModifier uc f):ucms) t 
    | Just x <- destruct uc t = construct uc (f x) 
    | otherwise = modifyHelper ucms t 

и, наконец, (лямбда могло быть (\xs -> [xs !! 0]) или (\xs -> [head xs]) по вкусу):

modify :: Block -> Block 
modify = modifyHelper [UnaryConstructorModifier para (\(x:_) -> [x]), 
         UnaryConstructorModifier plain (\(x:_) -> [x])] 

Если мы теперь проверить его с

ghci> let blocks = [Para [1,2,3,4], Plain ["a","b"], Other True (Para [42,43]), YetAnother ] 
ghci> map modify blocks == map modifyBaseline blocks 

мы получаем True - hooray!


Повторяющийся бит теперь в определениях para и plain, где вы должны написать имя конструктора три раза, но нет никакого способа обойти это без использования шаблона Haskell или Data.Data (что я могу видеть).

Других вариантов улучшения будут делать что-то для конструкторов различной арности и поставить функцию типа a -> Maybe a в UnaryConstructorModifier, чтобы иметь дело с необъективностью (\(x:_) -> [x]), но я думаю, что это ответ на ваш вопрос хорошо.

Я надеюсь, что вы можете понять это, как я приукрасил пару деталей, в том числе то, что происходит в определении UnaryConstructorModifier и использовании шаблонов охранников в modifyHelper - так спросить, если вам нужно уточнение!

3

Предполагая, что объявление типа данных в виде:

data Block 
    = Para { field :: [Something] } 
    | Plain { field :: [Something] } 
    ... 

Вы можете просто использовать общий синтаксис записи:

test :: [Block] -> [Block] 
test (x:rest) = x { field = [(field x) !! 0] } : rest 

Live demo

+0

Будет ли эта работа «Блока» также иметь другие подтипы (например, «BulletList»), которые я хочу обрабатывать отдельно? –

+0

@ Mathematical.coffee Что вы подразумеваете под «подтипами»? – Shoe

+0

Как в «Блоке» может быть 'Para',' Plain', 'BulletList', .... и я хочу обрабатывать' Para' и 'Plain' одинаково, но для всех остальных просто проходят через as-is ,Не уверен, что это помогает, но определение для «Блока» [здесь] (http://hackage.haskell.org/package/pandoc-types-1.8.2/docs/src/Text-Pandoc-Definition.html#Block) –

1

Ближайший вы можете получить к вашей первоначальной идее вероятно, является «общей» функцией Para -или- Plain -модификацией.

{-# LANGUAGE RankNTypes #-} 
modifyBlock :: (forall a . [a]->[a]) -> Block -> Block 
modifyBlock f (Plain foobar) = Plain $ f foobar 
modifyBlock f (Para foobar) = Plain $ f foobar 

Заметим, что f имеет в каждом использовании другого типа!

С, что вы можете написать

test (b:bs) = modifyBlock (\(h:_) -> [h]) b : bs 
1

Это может быть сделано очень красиво с ViewPatterns

Примечание: просмотреть шаблоны на самом деле здесь не требуется, он просто делает это выглядит немного лучше имхо

Примечание ': Это предполагает список внутри блока того же типа в обоих случаях

Конечно, вам все равно нужно выполнить сопоставление шаблонов где-то - вы не можете сделать это в общем случае без TH или Generic, но вы можете сделать это таким образом, чтобы использовать его повторно.

Смежные вопросы