在Haskell中实现拆分的单行实现

时间:2018-03-10 16:02:11

标签: haskell

我想要的是以下内容(我认为它应该包含在前奏中,因为它在文本处理中非常有用):

split :: Eq a => [a] -> [a] -> [[a]]

e.g:

split ";;" "hello;;world" = ["hello", "world"]
来自split

Data.List.Utils不在基础中。我觉得通过组合一些基本功能应该有一个简短的实现,但我无法弄清楚。我错过了什么吗?

1 个答案:

答案 0 :(得分:2)

可以说,最好的方法是检查短小甜蜜splitOn(或split的可行性,正如你和MissingH所说的那样 - 在这里我会坚持这个名字由splitextra软件包使用)试图编写它[注1]。

(顺便说一句,我会在这个答案中使用recursion-schemes函数和概念,因为我发现系统化的东西有助于我思考这类问题。如果有什么不清楚,请告诉我。)< / p>

splitOn的类型是[注2]:

splitOn :: Eq a => [a] -> [a] -> [[a]]

编写从另一个数据结构构建一个数据结构的递归函数的一种方法,如splitOn一样,首先通过在自下而上中走原始结构来询问是否这样做或自上而下方式(对于列表,分别为从右到左和从左到右)。自下而上的步行更自然地表达为某种折叠:

foldr @[] :: (a -> b -> b) -> b -> [a] -> b
cata @[_] :: (ListF a b -> b) -> [a] -> b

cata是catamorphism的缩写,递归方案表示一个vanilla fold。ListF a b -> b函数,称为代数 in行话,指定每个折叠步骤中发生的事情。data ListF a b = Nil | Cons a b,因此,在列表的情况下,代数相当于foldr的两个第一个参数汇总为一个 - 二进制函数对应于Cons个案,以及折叠的种子,到Nil个。)

另一方面,自上而下的行走适合展开:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a] -- found in Data.List
ana @[_] :: (b -> ListF a b) -> b -> [a]

ana是变形的缩写,是递归方案中的香草展开。b -> ListF a b函数是代数;它指定在每个展开步骤中会发生什么。对于列表,可能是发出列表元素和更新的种子或生成空列表并终止展开。)

splitOn应该是自下而上还是自上而下?为了实现它,我们需要在列表中的任何给定位置向前看,以检查当前列表段是否以分隔符开头。既然如此,有必要达到自上而下的解决方案,即展开/变形。

使用将splitOn作为展开方式编写的方法显示了另一个需要考虑的事项:您希望每个人展开步骤以生成完整形成的列表块。不这样做最多会导致你不必要地走两次原始列表[注3];在最坏的情况下,长列表块上的灾难性内存使用和堆栈溢出等待[注4]。实现这一目标的一种方法是通过breakOn函数,如Data.List.Extra ...

中的函数
breakOn :: Eq a => [a] -> [a] -> ([a], [a]) 

...这与Prelude中的break类似,不同之处在于,它不是对每个元素应用谓词,而是检查剩余列表段是否将第一个参数作为前缀[注释5]。 / p>

有了breakOn,我们可以编写一个正确的splitOn实现 - 一个用优化编译的实现与开头提到的库匹配的实现:

splitOnAtomic :: Eq a => [a] -> [a] -> [[a]]
splitOnAtomic delim
    | null delim = error "splitOnAtomic: empty delimiter"
    | otherwise = apo coalgSplit
    where
    delimLen = length delim
    coalgSplit = \case
        [] -> Cons [] (Left [])
        li ->
            let (ch, xs) = breakOn (delim `isPrefixOf`) li
            in Cons ch (Right (drop delimLen xs))

apo,是apomorphism的缩写,是一种可以被短路的展开。这是通过从展开步骤发出而不是通常更新的种子 - 由Right发出信号 - - 最终结果 - 由Left发出信号。这里需要短路,因为在空列表的情况下,我们不希望通过返回Nil来产生空列表 - 这会错误地导致splitOn delim [] = [] - 也不会诉诸Cons [] [] - 这将产生[]的无限尾部。此技巧直接对应于{{3}添加的splitOn _ [] = [[]]案例}}。)

经过一些轻微的弯路,我们现在可以解决您的实际问题。 splitOn以简短的方式编写是很棘手的,因为首先,它使用的递归模式并不是完全无关紧要的;其次,良好的实施需要一些不便于打高尔夫球的细节;第三,看起来最好的实现主要依赖于breakOn,而不是 base

注意:

[注1]:以下是在此答案中运行代码段所需的导入和编译指示:

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TemplateHaskell #-}

import Data.Functor.Foldable
import Data.Functor.Foldable.TH
import Data.List
import Data.Maybe

[注2]:替代类型可能是Eq a => NonEmpty a -> [a] -> NonEmpty [a],如果想要将精度放在首位以上。我不打扰这里,以避免不必要的干扰。

[注3]:正如在这个相当简洁的实现中,它使用两个展开 - 第一个(ana coalgMark)用Nothing替换分隔符,以便第二个(apo coalgSplit)可以直接分割:

splitOnMark :: Eq a => [a] -> [a] -> [[a]]
splitOnMark delim
    | null delim = error "splitOnMark: empty delimiter"
    | otherwise = apo coalgSplit . ana coalgMark
    where
    coalgMark = \case
        [] -> Nil
        li@(x:xs) -> case stripPrefix delim li of
            Just ys -> Cons Nothing ys
            Nothing -> Cons (Just x) xs
    coalgSplit = \case
        [] -> Cons [] (Left [])
        mxs ->
            let (mch, mys) = break isNothing mxs
            in Cons (catMaybes mch) (Right (drop 1 mys))

apo是什么以及LeftRight在这里做了什么将在答案的主体中进一步介绍。)

这种实现具有相当可接受的性能,但是通过优化,它比答案主体中的(适度的)常数因子慢。打高尔夫球可能要容易一点,但是......

[注4]:就像在这个单一的展开实现中一样,它使用了一个以递归方式调用自身来构建每个块作为(差异)列表的代数:

splitOnNaive :: Eq a => [a] -> [a] -> [[a]]
splitOnNaive delim 
    | null delim = error "splitOn: empty delimiter"
    | otherwise = apo coalgSplit . (,) id
    where
    coalgSplit = \case
        (ch, []) -> Cons (ch []) (Left [])
        (ch, li@(x:xs)) -> case stripPrefix delim li of
            Just ys -> Cons (ch []) (Right (id, ys))
            Nothing -> coalg (ch . (x :), xs)

必须决定每个元素是否增长当前的块或开始一个新的元素本身是有问题的,因为它打破了懒惰。

[注5]: Data.List.Extra implementation。如果我们想要使用 recursion-schemes 展开来实现这一目标,那么一个好的策略是定义一个数据结构,它正好编码我们正在尝试构建的内容:

data BrokenList a = Broken [a] | Unbroken a (BrokenList a)
    deriving (Eq, Show, Functor, Foldable, Traversable)
makeBaseFunctor ''BrokenList

BrokenList就像一个列表,除了空列表被(非递归)Broken构造函数替换,它构造了断点并保留了列表的其余部分。一旦展开生成,BrokenList就可以很容易地折叠成一对列表:Unbroken值中的元素被压缩到一个列表中,Broken中的列表变成另一个列表之一:

breakOn :: ([a] -> Bool) -> [a] -> ([a], [a])
breakOn p = hylo algPair coalgBreak
    where
    coalgBreak = \case
        [] -> BrokenF []
        li@(x:xs)
            | p li -> BrokenF li
            | otherwise -> UnbrokenF x xs
    algPair = \case 
        UnbrokenF x ~(xs, ys) -> (x : xs, ys)
        BrokenF ys -> ([], ys)

hylo是hylomorphism的缩写,只是ana后跟cata,即展开后跟折叠。hylo,在递归方案,利用了这样一个事实,即由展开创建然后立即被折叠消耗的中间数据结构可以被融合掉,从而显着提高性能。)

值得一提的是algPair中的懒惰模式匹配对于保持懒惰至关重要。与上述相关联的Data.List.Extra实现通过使用first中的Control.Arrow来实现,{{1}}也与懒惰地提供的对匹配。