Общая схема рекурсии
Я привык к более высоким функциям Haskell. Обычно я могу заменить явные шаблоны рекурсии такими функциями, как map, fold и scan. Однако я часто сталкиваюсь с следующей схемой рекурсии, которую я не понимаю, как выразить, используя функции более высокого порядка:
f (x:[]) = k x
f (x:xs) = g x (f xs)
Например, предположим, что я представляю аналитические таблицы. Затем я создаю тип данных, например:
data Tableau = N Expr | S Expr (Tableau) | B Expr (Tableau) (Tableau)
Если я хочу преобразовать список Expr
в структуру таблицы, мне нужна функция, часть которой может напоминать:
f (x:[]) = N x
f (x:xs) = S x (f xs)
Теперь я вижу три варианта: (1) создать функцию, которая решает, учитывая таблицу и список, должна ли следующая ветвь в таблице быть S
или N
(или B
, но мы проигнорируем этот случай); (2) использовать функцию более высокого порядка для инкапсуляции рекурсивного рисунка f
; (3) используйте функцию типа f
.
Каким будет лучший вариант?
Ответы
Ответ 1
Я бы, скорее всего, использовал следующее:
f xs = foldr g (k (last xs)) (init xs)
В основном это означает, что конец списка заменяется на k x
при складывании. Благодаря ленивой оценке, присутствующей повсюду, она работает даже для бесконечных списков.
Есть два других решения - добавление пустого случая и использование Maybe.
A) добавление пустого случая:
Было бы лучше, если бы f []
был четко определен. Затем вы можете написать определение как
f [] = c
f (x:xs) = g x (f xs)
который равен f = foldr g c
. Например, если вы измените
data Tableau = N Expr | S Expr Tableau | B Expr Tableau Tableau
к
data Tableau = N | S Expr Tableau | B Expr Tableau Tableau
то вы можете представить одноэлементные таблицы как S expr N
, а функция определена как однострочный
f = foldr S N
Это лучшее решение, поскольку пустой случай имеет смысл.
B) use Возможно:
С другой стороны, если f []
нельзя определить разумно, это хуже.
Частичные функции часто считаются уродливыми. Чтобы сделать его общим, вы можете использовать Maybe
. Определение
f [] = Nothing
f [x] = Just (k x)
f (x:xs) = Just (g x w)
where Just w = f xs
Это полная функция - это лучше.
Но теперь вы можете переписать эту функцию на:
f [] = Nothing
f (x:xs) = case f xs of
Nothing -> Just (k x)
Just w -> Just (g x w)
которая является правой складкой:
addElement :: Expr -> Maybe Tableaux -> Maybe Tableaux
addElement x Nothing = Just (N x)
addElement x (Just w) = Just (S x w)
f = foldr addElement Nothing
В целом, сгибание является идиоматическим и должно использоваться, когда вы вписываетесь в шаблон рекурсии. В противном случае используйте явную рекурсию или попробуйте повторно использовать существующие комбинаторы. Если есть новый шаблон, сделайте комбинатор, но только если вы будете использовать шаблон много, иначе он будет излишним. В этом случае шаблон складывается для непустых списков, определяемых: data List a = End a | Cons a (List a)
.
Ответ 2
Если я правильно понял вопрос, то здесь моя оценка ваших вариантов:
-
Возможно, немного неприятно сопоставлять таблицу (предположительно произвольно сложную?) из-под конструктора, чтобы написать эту функцию. Этот подход кажется несколько хрупким, хотя он, вероятно, будет работать нормально.
-
Я не вижу необходимости обобщать шаблон, учитывая, что это рекурсивная функция, работающая на рекурсивной структуре. Представление шаблона более высокого порядка (я думаю) обфускает фактическую логику выполнения рекурсивного обхода этой структуры данных.
-
Я думаю, что это имеет большой смысл. Как вы заметили, это разумно признанный "шаблон", но я думаю, что он точно соответствует описанию алгоритма, чтобы записать его именно таким образом. Возможно, он не может быть обобщенным или многоразовым, но учитывая, что он по существу является основным алгоритмом алгоритмического подхода, я считаю, что имеет смысл писать случаи напрямую, как вы это делали в функции типа f. Это был бы мой любимый подход.
Извините, что не дает особо конкретного ответа, но это достаточно субъективный ответ, поэтому, учитывая три варианта выше, я бы выбрал вариант 3 из соображений ясности и удобочитаемости.