Как написать эту функцию идиоматически?

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

Пример:

someFunction :: (a -> Bool) -> (a -> a) -> [a] -> [[a]]
someFunction = ...

let ys = someFunction isOdd (* 2) [1..10]
    {- ys == [[2, 2, 3, 4, 5,  ...],
              [1, 2, 6, 4, 5,  ...],
              [1, 2, 3, 4, 10, ...],
              ...] -}

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

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


person aochagavia    schedule 25.09.2014    source источник


Ответы (4)


Вы можете использовать палец (например, молнию: D, вы проводите пальцем по каждому элементу: D как когда ты читаешь)

someFunction :: (a -> Bool) -> (a -> a) -> [a] -> [[a]]
someFunction check f xs = r [] xs
  where r _  []     = []
        r ps (y:ys) = let rs = r (ps ++ [y]) ys
                      in  if check y then [ps ++ [f y] ++ ys] ++ rs
                                     else rs

Функция r принимает ps "обработанные элементы" и (y:ys) "ожидающие элементы".

Если вам нужна линейная стоимость (операция ps ++ [y] делает ее квадратичной), используйте эффективную структуру вставки хвоста.

Используя splitAt, вы можете написать

someFunction check f xs = map (\(a,(x:b)) -> a ++ [f x] ++ b) $
                          filter (check.head.snd)
                          [splitAt n xs | n <- [0..length xs - 1]]

Или используя понимание списка

someFunction check f xs =
    [ a ++ [f x] ++ b | n <- [0..length xs - 1]
                      , let (a, (x:b)) = splitAt n xs
                      , check x]

Используя zip, предложенный @chi, решение требует линейной стоимости (генерация списков, наконец, O (n ^ 2))

someFunction check f xs = 
    [ a ++ [f x] ++ b | (a, (x:b)) <- init $ zip (inits xs) (tails xs)
                      , check x]

Наконец (?) @ØrjanJohansen примечание об удалении init $ (я оставляю обе версии, думаю, это отличный пример)

Избегайте init $

someFunction check f xs = 
    [ a ++ [f x] ++ b | (a, (x:b)) <- zip (inits xs) (tails xs)
                      , check x]

последний (xs, []) "заархивированный" элемент избегается пониманием списка, @ØrjanJohansen указал здесь как это переводится

[e | p <- l, Q] = let ok p = [e | Q]
                      ok _ = []
                  in concatMap ok l

(спасибо @WillNess)

person josejuan    schedule 25.09.2014
comment
Скользящая линза также может рассматриваться как палец. - person Bartek Banachewicz; 25.09.2014
comment
Последнее предложение zip, по сути, является идиомой, которую я бы использовал, чтобы написать это быстро, но я просто добавлю примечание, что вам не нужна часть init $ - последняя пара автоматически отфильтровывается шаблоном (a, (x:b)). - person Ørjan Johansen; 26.09.2014
comment
@ØrjanJohansen Я думал, что будет сообщено об ошибке Non-exhaustive patterns. Отличный пример! Спасибо! - person josejuan; 26.09.2014
comment
Нотация desugar do не работает, потому что обычно простое do p <- e1; e2 -> e1 >>= \p -> e2 не на самом деле точная официальная дешугаризация - и этот случай, когда p является шаблоном, который может дать сбой, именно там, где он ломается. - person Ørjan Johansen; 26.09.2014
comment
понимание списка без сахара - это код, использующий concatMap, а не do... :) - person Will Ness; 26.09.2014

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

Я определяю тип «списков с одним отверстием элемента» следующим образом:

data Bwd x = B0 | Bwd x :< x deriving Show
type HoleyList x = (Bwd x, [x])

Строго говоря, мне не нужно вводить обратные списки, чтобы сделать это, но я так легко запутаюсь, если мне нужно перевернуть что-то в своей голове. (Так получилось, что HoleyList является формальной производной от [].)

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

type InContext x = (HoleyList x, x)

Идея состоит в том, что второй компонент пары находится между обратным списком и прямым списком. Я могу определить функцию, которая снова соединяет список (называется upF в общей обработке).

plug :: InContext x -> [x]
plug ((B0, xs), y)      = y : xs
plug ((xz :< x, xs), y) = plug ((xz, y : xs), x)

Я также могу определить функцию, которая дает все способы разобрать список на части (downF, в общем случае).

selections :: [x] -> [InContext x]
selections = go B0 where
  go xz [] = []
  go xz (x : xs) = ((xz, xs), x) : go (xz :< x) xs

Обратите внимание, что

map snd  (selections xs) = xs 
map plug (selections xs) = map (const xs) xs

И теперь мы можем следовать рецепту Бартека.

selectModify :: (a -> Bool) -> (a -> a) -> [a] -> [[a]]
selectModify p f = map (plug . (id *** f)) . filter (p . snd) . selections

То есть: отфильтруйте выборки по тесту, примените функцию к элементу в фокусе, затем соедините обратно. Если у вас есть застежка-молния, то это однострочник, и он должен работать для любого дифференцируемого функтора, а не только для списков! Работа выполнена!

> selectModify ((1 ==) . (`mod` 2)) (2*) [1..10]
[[2,2,3,4,5,6,7,8,9,10]
,[1,2,6,4,5,6,7,8,9,10]
,[1,2,3,4,10,6,7,8,9,10]
,[1,2,3,4,5,6,14,8,9,10]
,[1,2,3,4,5,6,7,8,18,10]]
person pigworker    schedule 25.09.2014

Как насчет этого:

Начните со списка:

[1,2,3,4]

Скопируйте список n раз, где n — его размер (:: [[]]):

[
 [1,2,3,4],
 [1,2,3,4],
 [1,2,3,4],
 [1,2,3,4]
]

Разделите списки на каждый элемент (более или менее "по диагонали") (:: [([], [])]):

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

Отфильтруйте строки, в которых head . snd не удовлетворяет вашему предикату

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

Примените свою функцию к оставшимся головкам

[
 ([],    [2,2,3,4])
 ([1,2], [6,4]),
]

Объединить пары обратно

[
 [2,2,3,4],
 [1,2,6,4]
]
person Bartek Banachewicz    schedule 25.09.2014
comment
Что вы имеете в виду под диагональю? - person aochagavia; 25.09.2014
comment
Позвольте мне расширить это немного. - person Bartek Banachewicz; 25.09.2014
comment
@aochagavia вот так. - person Bartek Banachewicz; 25.09.2014
comment
Может быть, проще пропустить шаг копирования и напрямую заархивировать инициалы и хвосты. - person chi; 25.09.2014

Просматривая все хорошие и в основном несколько причудливые решения, размещенные здесь (включая последнее решение @josejuan, основанное на zip, которое, по сути, является идиомой, которую я бы использовал сам в спешке), я не могу отделаться от ощущения, что в списке отсутствует действительно прямое, но все же идиоматическое решение, использующее явную, ленивую рекурсию - вид кода, который вы, вероятно, видели бы в стандартных библиотеках, если бы someFunction была стандартной функцией. Итак, вот версия этого (включая обертку go worker, которую вы также видели):

someFunction :: (a -> Bool) -> (a -> a) -> [a] -> [[a]]
someFunction p f xs = go xs where
    go [] = []
    go (x:xs)
      | p x         = (f x : xs) : rest
      | otherwise   = rest
      where
        rest = map (x :) (go xs)
person Ørjan Johansen    schedule 25.09.2014