Чистый синтаксис для условного сворачивания списка в Haskell

Я относительно новичок в haskell, но в своих поисках я не смог найти простой способ условно свернуть список. т. е. когда элемент удовлетворяет условию (например, filter), чтобы свернуть этот элемент с помощью функции (например, foldr и foldl).

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

-- This function returns tuples containing the elements which 
--    satisfy `cond` folded right, adding 1 to the second value
-- in each pair. (`snd` pair starts at 0)
-- Condition takes a single value (similar to `filter`)
-- NOTE: list cannot end with token
foldrOn cond list = 
    if (length list) > 0 then 
        if cond (head list) then 
            do
                let tmp = foldrOn cond (tail list)
                (fst (head tmp), snd (head tmp) + 1) : (tail tmp) 
                      -- fold token into char after it 
        else
            (head list, 0) : (foldrOn cond (tail list)) 
                      -- don't fold token
    else
        [] -- base case len list = 0

foldlOn cond list = ...

Например, вариант использования может быть чем-то вроде желания удалить нули в следующих списках, но помнить, сколько было удалено между каждым значением.

-- the second value in each resultant pair represents the number of 
-- zeroes preceding the corresponding first value in the original list.
foldrOn (== 0) [1,0,0,0,0,0,1,0,0,0,1] -- [(1,0),(1,5),(1,3)]
foldrOn (== 0) [1,0,0,12,0,13] -- [(1,0),(12,2),(13,1)]

Есть ли лучший способ сделать это? Кроме того, можно ли это сделать более оптимально?


person EarthenSky    schedule 23.12.2020    source источник
comment
Ваш foldrOn не получает аргумент двоичной функции, поэтому на самом деле это не fold. Пожалуйста, покажите желаемый результат вашего варианта использования.   -  person n. 1.8e9-where's-my-share m.    schedule 23.12.2020
comment
Не могли бы вы отредактировать свой вопрос, включив несколько примеров использования и ожидаемого результата? Я попытался выполнить foldrOn (== 0) с обоими входными данными из вашего примера, но я не понимаю, как фактическое поведение согласуется с заявленной целью.   -  person Mark Seemann    schedule 23.12.2020
comment
Спасибо за ответ! Я внес изменения, которые должны облегчить понимание вопроса, и включил несколько примеров.   -  person EarthenSky    schedule 23.12.2020
comment
Сделав шаг назад, вы действительно не хотите выполнять кодирование длин серий?   -  person Mark Seemann    schedule 23.12.2020
comment
вам не нужно это do. do { let {a=b} ; c } полностью совпадает с let {a=b} in do c, а do c — это просто c, когда c — это всего лишь одно выражение.   -  person Will Ness    schedule 23.12.2020
comment
Я призываю вас избегать использования head и tail... почти никогда. Я знаю, что это Haskell, но читается как Lisp. Вычислять length списка и проверять, равен ли он нулю, (практически) никогда не бывает хорошей идеей. Обычно вы хотите либо использовать стандартное свертывание/обход, либо сопоставление с шаблоном в списке, чтобы определить, пуст ли он, и, если это не так, разложить его на начало и конец. Почти во всех других случаях вы должны использовать null, чтобы проверить, пуст ли список.   -  person dfeuer    schedule 23.12.2020


Ответы (3)


Прежде всего,

foldrOn :: Num t => (a -> Bool) -> [a] -> [(a, t)]
-- foldrOn (== 0) [1,0,0,0,0,0,1,0,0,0,1] -- [(1,0),(1,5),(1,3)]
foldrOn p xs  =  foldr g [] xs
  where
  g x []        = [(x,0)]
  g x ((y,n):r)
    | p x       = ((y,n+1):r)
  g x r         = ((x,0):r)

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

Чтобы сделать его максимально ленивым, нам пришлось бы использовать ленивый левый сгиб. Пропуск элементов, удовлетворяющих p, по-прежнему является рекурсивным шагом, но, по крайней мере, процесс будет приостанавливаться между каждым таким интервалом.

Ленивая левая складка обычно реализуется как foldr с дополнительным аргументом, передаваемым слева направо по списку:

foldlOn :: Num t => (a -> Bool) -> [a] -> [(a, t)]
-- foldlOn (== 0) [1,0,0,0,0,0,1,0,0,0,1] -- [(1,0),(1,5),(1,3)]
foldlOn p xs  =  foldr g z xs 0
  where
  g x r i | p x       =         r (i+1)
          | otherwise = (x,i) : r 0
  z    _i             =         []

Или вы можете объединить span/break и unfoldr, чтобы сделать то же самое.

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

GHCi> groupBy (\a b -> (==0) b) [1,0,0,0,0,0,1,0,0,0,1]
[[1,0,0,0,0,0],[1,0,0,0],[1]]

GHCi> groupBy (const (==0)) [1,2,0,0,1,0,1]
[[1],[2,0,0],[1,0],[1]]

Закончить это не должно быть проблемой.

person Will Ness    schedule 23.12.2020

Вы всегда можете привезти встроенную технику. Библиотека Data.List довольно мощная:

import Data.List(mapAccumL)
import Data.Maybe(catMaybes)

foldrOn cond = catMaybes . snd . mapAccumL combine 0 where
  combine a el =
    if cond el then (a + 1, Nothing)
    else (0, Just (el, a))

Что происходит

По сути, foldrOn cond представляет собой композицию следующих функций:

  • mapAccumL combine 0, который продвигается по списку, изменяя каждый элемент информацией о количестве недавно пропущенных объектов (начиная отсчет с 0 и сбрасывая его всякий раз, когда мы находим что-то, что не соответствует предикату cond).
  • snd, который отбрасывает конечное состояние из результата mapAccumL
  • catMaybes, который удаляет слой Maybe и оставляет только текущие значения.
person radrow    schedule 23.12.2020
comment
красивый. смысл гораздо более очевиден таким образом. ---- предлагаемое изменение: mapAccumL combine 0 продвигается по списку... недавно пропущенные объекты, [начиная с 0]. (к сожалению, термин traverse немного перегружен в Haskell) - person Will Ness; 23.12.2020
comment
Спасибо, подал заявку! - person radrow; 23.12.2020

Давайте начнем с использования сопоставления с образцом, чтобы сделать вашу собственную реализацию более идиоматической, более очевидно правильной, а также (намного) более быстрой. Мы также можем использовать guards идиоматически, а не if/then/else; это скорее менее важно. Здесь также нет причин использовать do, поэтому мы не будем.

foldrOn _cond [] = []
foldrOn cond (hd : tl)
  | cond hd
  = case foldrOn cond tl of
      (x, y) : tl' -> (x, y + 1) : tl' 
                      -- fold token into char after it
      [] -> error "String ended on token."
  | otherwise
  = (hd, 0) : foldrOn cond tl
                      -- don't fold token

Это нормально. Но, как предполагает Уилл Несс, на самом деле мы ничего не выигрываем, добавляя незавершенный элемент в список результатов. Вместо этого мы можем подсчитывать cond-удовлетворяющие токены, пока не достигнем конца блока, а затем создать полный элемент. Я думаю, что это делает код немного более понятным, и он также должен работать немного быстрее.

foldrOn cond = go 0
  where
    go count (hd : tl)
      | cond hd
      = go (count + 1) tl -- Don't produce anything; just bump the count
      | otherwise
      = (hd, count) : go 0 tl -- Produce the element and the count; reset the count to 0
    go count []
      | count == 0
      = []
      | otherwise
      = error "List ended on a token."

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

-- At the top of the file, add this line:
{-# LANGUAGE BangPatterns #-}

foldrOn cond = go 0
  where
    go !count (hd : tl)
      | cond hd
      = go (count + 1) tl -- Don't produce anything; just bump the count
      | otherwise
      = (hd, count) : go 0 tl -- Produce the element and the count; reset the count to 0
    go count []
      | count == 0
      = []
      | otherwise
      = error "List ended on a token."

Это можно записать как складку, как это демонстрирует Уилл Несс.

Примечание: хотя и можно избежать языкового расширения BangPatterns, это немного раздражает.

person dfeuer    schedule 23.12.2020
comment
«Раздражающий» эквивалент с seq вместо BangPatterns на самом деле довольно прост с точки зрения кода. f !x = e эквивалентно f x = seq x e, поэтому получается go count (hd : tl) | cond hd = count `seq` go (count + 1) tl | otherwise = count `seq` ((hd, count) : go 0 tl). Объяснение seq выходит за рамки этого комментария, но все, что он делает, — это добавляет зависимость оценки: здесь if оценивается результат (что происходит всякий раз, когда вызывающая программа проверяет следующий конструктор списка, используя сопоставление с образцом) затем также оценивается предыдущее значение count. - person Jon Purdy; 23.12.2020
comment
@JonPurdy, да, я знаю. Но необходимость повторять seq дважды раздражает, когда я знаю, что мне это, вероятно, нужно только один раз при компиляции с оптимизацией. И так шумно в коде! - person dfeuer; 24.12.2020
comment
Конечно, я подумал, что вы знаете, мой комментарий был для наших дорогих читателей :) - person Jon Purdy; 24.12.2020
comment
Я отредактировал, чтобы увидеть эффект от этого. здесь был длинный комментарий, который я позже удалил, о том, как структура визуального кода должна следовать за внутренней структурой кода - охранники слева, код предложений справа от знака равенства.... но даже добавление всего четырех пробелов перед каждым из = (и соответствующий отступ их кода) в вашем коде, как сейчас, очень помогает превратить эту линейную стену кода в древовидную, что, на мой взгляд, резко улучшает читаемость. ММВ конечно.... - person Will Ness; 25.12.2020