Я относительно новичок в 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)]
Есть ли лучший способ сделать это? Кроме того, можно ли это сделать более оптимально?
Не могли бы вы отредактировать свой вопрос, включив несколько примеров использования и ожидаемого результата? Я попытался выполнить foldrOn (== 0)
с обоими вашими примерами ввода, но я не понимаю, как фактическое поведение согласуется с заявленной целью.
Спасибо за ответ! Я внес изменения, которые должны облегчить понимание вопроса, и включил несколько примеров.
Сделав шаг назад, вы действительно не хотите выполнять кодирование длин серий?
тебе это не нужно do
. do { let {a=b} ; c }
полностью совпадает с let {a=b} in do c
, а do c
— это просто c
, когда c
— это всего лишь одно выражение.
Я призываю вас избегать использования head
и tail
... почти никогда. Я знаю, что это Haskell, но читается как Lisp. Вычислять length
списка и проверять, равен ли он нулю, (практически) никогда не бывает хорошей идеей. Обычно вы хотите либо использовать стандартную складку/обход, либо сопоставление с образцом в списке, чтобы определить, является ли он пустым, и, если это не так, разобрать его на голову и хвост. Почти во всех других случаях вы должны использовать null
, чтобы проверить, пуст ли список.
Прежде всего,
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]]
Закончить это не должно быть проблемой.
Вы всегда можете привезти встроенную технику. Библиотека 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
и оставляет только «настоящие» значения.хороший. смысл гораздо более очевиден таким образом. ---- предложил отредактировать: «mapAccumL combine 0
который продвигается по списку… недавно пропущенные объекты, [начиная с 0]». (к сожалению, "переход" - это немного перегруженный термин в Haskell)
Давайте начнем с использования сопоставления с образцом, чтобы сделать вашу собственную реализацию более идиоматической, более очевидно правильной, а также (намного) более быстрой. Мы также можем использовать охранников идиоматически, а не 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
, это немного раздражает.
«Раздражающий» эквивалент с 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
выходит за рамки этого комментария, но все, что он делает, это добавляет зависимость оценки: здесь, если результат оценивается (что происходит всякий раз, когда вызывающий объект проверяет следующий конструктор списка с использованием сопоставления с образцом), то предыдущее значение count
также оценивается.
@JonPurdy, да, я знаю. Но необходимость повторять seq
дважды раздражает, когда я знаю, что мне это, вероятно, понадобится только один раз при компиляции с оптимизацией. И так шумно в коде!
Конечно, я подумал, что вы знаете, мой комментарий был для наших дорогих читателей :)
Я отредактировал, чтобы увидеть эффект от этого. здесь был длинный комментарий, который я позже удалил, о том, как структура визуального кода должна следовать структуре внутреннего кода - охранники слева, код предложений справа от знака равенства.... но даже добавление всего четырех пробелов перед каждым из =
s (и соответствующий отступ их кода) в вашем коде, как сейчас, очень помогает превратить эту линейную «стену кода» в древовидную, что, на мой взгляд, резко улучшает читаемость. ММВ конечно....
Ваш
foldrOn
не получает аргумент двоичной функции, поэтому на самом деле это неfold
. Пожалуйста, покажите желаемый результат вашего варианта использования.