Чистый синтаксис для условного сворачивания списка в 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)]

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

Ваш foldrOn не получает аргумент двоичной функции, поэтому на самом деле это не fold. Пожалуйста, покажите желаемый результат вашего варианта использования.

n. m. 23.12.2020 09:10

Не могли бы вы отредактировать свой вопрос, включив несколько примеров использования и ожидаемого результата? Я попытался выполнить foldrOn (== 0) с обоими вашими примерами ввода, но я не понимаю, как фактическое поведение согласуется с заявленной целью.

Mark Seemann 23.12.2020 09:10

Спасибо за ответ! Я внес изменения, которые должны облегчить понимание вопроса, и включил несколько примеров.

EarthenSky 23.12.2020 09:23

Сделав шаг назад, вы действительно не хотите выполнять кодирование длин серий?

Mark Seemann 23.12.2020 09:38

тебе это не нужно do. do { let {a=b} ; c } полностью совпадает с let {a=b} in do c, а do c — это просто c, когда c — это всего лишь одно выражение.

Will Ness 23.12.2020 11:27

Я призываю вас избегать использования head и tail... почти никогда. Я знаю, что это Haskell, но читается как Lisp. Вычислять length списка и проверять, равен ли он нулю, (практически) никогда не бывает хорошей идеей. Обычно вы хотите либо использовать стандартную складку/обход, либо сопоставление с образцом в списке, чтобы определить, является ли он пустым, и, если это не так, разобрать его на голову и хвост. Почти во всех других случаях вы должны использовать null, чтобы проверить, пуст ли список.

dfeuer 23.12.2020 17:53
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
687
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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]]

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

Вы всегда можете привезти встроенную технику. Библиотека 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)

Will Ness 23.12.2020 14:24

Давайте начнем с использования сопоставления с образцом, чтобы сделать вашу собственную реализацию более идиоматической, более очевидно правильной, а также (намного) более быстрой. Мы также можем использовать охранников идиоматически, а не 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 также оценивается.

Jon Purdy 23.12.2020 21:12

@JonPurdy, да, я знаю. Но необходимость повторять seq дважды раздражает, когда я знаю, что мне это, вероятно, понадобится только один раз при компиляции с оптимизацией. И так шумно в коде!

dfeuer 24.12.2020 01:27

Конечно, я подумал, что вы знаете, мой комментарий был для наших дорогих читателей :)

Jon Purdy 24.12.2020 21:09

Я отредактировал, чтобы увидеть эффект от этого. здесь был длинный комментарий, который я позже удалил, о том, как структура визуального кода должна следовать структуре внутреннего кода - охранники слева, код предложений справа от знака равенства.... но даже добавление всего четырех пробелов перед каждым из = s (и соответствующий отступ их кода) в вашем коде, как сейчас, очень помогает превратить эту линейную «стену кода» в древовидную, что, на мой взгляд, резко улучшает читаемость. ММВ конечно....

Will Ness 25.12.2020 01:27

Другие вопросы по теме