如何在Haskell中反转NonEmpty

huangapple go评论68阅读模式
英文:

How to reverse a NonEmpty in Haskell

问题

这次,我面临着颠倒一个 NonEmpty 的任务。

我尝试了以下代码(我知道它不符合我们的要求,但假设这是一个最小可行示例):

reverseNonEmpty :: NonEmpty a -> NonEmpty a
reverseNonEmpty (x :| []) = x :| []
reverseNonEmpty (x :| xs) = (x :| tail xs)

main :: IO ()
main = do
  print (reverseNonEmpty (1 :| [2, 3, 4])) -- 打印 "1 :| [3,4]"

上面的代码片段是第一个可以编译的版本。然而,我对 NonEmpty 的 Haskell 语法感到困惑(我在这方面是初学者)。

Q:如何使它颠倒输入的 NonEmpty

英文:

This time, I am faced with the task of reversing a NonEmpty.

I am trying this (I know that it does not do what we require it to, yet assume this is a MWE):

reverseNonEmpty :: NonEmpty a -> NonEmpty a
reverseNonEmpty (x :| []) = x :| []
reverseNonEmpty (x :| xs) = (x :| tail xs)

main :: IO ()
main = do
  print (reverseNonEmpty (1 :| [2, 3, 4])) -- Prints "1 :| [3,4]"

The above snippet is the first version that compiles. However, I am puzzled with Haskell syntax for NonEmpty's (I am a beginner here).

Q: How do I make it reverse input NonEmptys?

答案1

得分: 4

你可以这样用reverse函数来定义非空列表的翻转:

reverseNonEmpty :: NonEmpty a -> NonEmpty a
reverseNonEmpty (x :| xs) = case Prelude.reverse xs of
                            []     -> x :| []
                            (y:ys) -> y :| (ys ++ [x])

然而,这种方法并不是最优的,因为它会两次遍历列表,分别是reverse++操作。

另一种可能的方法是使用一个累加器辅助函数,例如:

reverseNonEmpty (x :| xs) = reverseAcc xs (x :| [])

reverseAcc :: [a] -> NonEmpty a -> NonEmpty a
reverseAcc []     acc = acc
reverseAcc (x:xs) acc = reverseAcc xs (x :| toList acc)

这个解决方案会反复构建和解构非空构造器 :|

Data.List.NonEmpty中,翻转函数被定义为列表翻转函数的提升:

reverse = lift List.reverse

这种方法是最有效的,因为它执行了一些常数时间的转换,一个线性遍历和最终的常数时间转换。

英文:

You can define it in terms of the reverse function for lists like this:

reverseNonEmpty :: NonEmpty a -> NonEmpty a
reverseNonEmpty (x :| xs) = case Prelude.reverse xs of
                            []     -> x :| []
                            (y:ys) -> y :| (ys ++ [x])

This approach however is not optimal because it traverses the list twice, reverse and ++.

Another possibility is with an accumulator helper function for example:

reverseNonEmpty (x :| xs) = reverseAcc xs (x :| [])

reverseAcc :: [a] -> NonEmpty a -> NonEmpty a
reverseAcc []     acc = acc
reverseAcc (x:xs) acc = reverseAcc xs (x :| toList acc)

This solution repeatedly constructs and destructs the nonempty constructor :|.

In Data.List.NonEmpty the reverse function is defined as a lifting of the list reverse function:

reverse = lift List.reverse

This approach is the most efficient, because it does some constant time conversion, a linear pass and a final constant time conversion.

答案2

得分: 3

以下是翻译好的部分:

"另一个现有的答案很好地描述了实现的各种替代方案,但这些替代方案似乎是凭空出现的。在这个答案中,我将尝试展示如何自己发明答案。我将从假设您已经了解了在普通列表上实现reverse的标准技巧开始,然后展示如何通过简单的本地转换将该实现转变为适用于非空列表的实现。

以下是普通列表的reverse的标准实现:

reverse xs = go [] xs where
go as [] = as
go as (b:bs) = go (b:as) bs

我们希望修改这个实现,使得我们的递归函数仅在非空列表上调用(即我们知道不是空的普通旧列表,而不是NonEmpty列表)。因此,无论我们何时调用go,在进行调用之前,我们都将执行模式匹配。如果我们本来会在空列表上调用go,我们将内联go本来会执行的操作,以便我们不必调用它。例如,不再使用go [] xs,而是在xs上进行一个情况检查,检查它是否为空,并仅在它不为空时调用go,如下所示:

reverse [] = [] -- 内联调用 go [] []
reverse (x:xs) = go [] (x:xs) {- 不内联,执行通常会执行的相同调用 -} where
go as [] = as -- 与以前完全没有变化,因为没有递归调用
go as (b:[]) = b:as -- 内联调用 go (b:as) []
go as (b:b'bs) = go (b:as) (b'b:bs) -- 不内联,执行通常会执行的相同调用

现在,我们知道go的参数始终是非空的,我们可以从go的定义中删除该情况。所有其他情况保持完全相同。

reverse [] = []
reverse (x:xs) = go [] (x:xs) where
go as (b:[]) = b:as
go as (b:b'bs) = go (b:as) b'bs

现在,我们在go的第二个参数上进行模式匹配以提取列表的第一个元素似乎有点奇怪,因为我们知道所有调用者刚刚已经执行了相同的模式匹配。所以让我们稍微改变调用约定:调用者将已经匹配的第一个列表元素单独传递。像这样:

reverse [] = []
reverse (x:xs) = go [] x xs where
go as b [] = b:as
go as b (b'bs) = go (b:as) b'bs

有了这个定义,现在非常容易实现weirdReverse :: NonEmpty a -> [a]:只需忽略空输入的第一个定义子句,因为我们知道它们不存在!当然,我们还必须在外部模式匹配中将:更改为:|

weirdReverse :: NonEmpty a -> [a]
weirdReverse (x:|xs) {- 交换构造函数 -} = go [] x xs where
go as b [] = b:as
go as b (b'bs) = go (b:as) b'bs

最后,由于现在go在语法上始终返回至少包含一个元素的列表,所以很容易将其返回值中的:替换为:|,以返回一个NonEmpty而不是[]

neReverse :: NonEmpty a -> NonEmpty a
neReverse (x:|xs) = go [] x xs where
go as b [] = b:|as -- 交换构造函数
go as b (b'bs) = go (b:as) b'bs

这种模式——在本质上与其[]对应物基本相同地实现NonEmpty函数,但“展开”每个定义一层并添加一个已知头部的参数——是相当通用的;它应该适用于诸如mapfoldrziplastinit等等的函数。我鼓励您尝试一下,因为这是一个绝佳的练习!如果您想从一个特别相似的函数开始,可以尝试last。"

英文:

The other extant answer does a good job of describing various alternatives for implementations, but those alternatives seem to appear out of whole cloth. In this answer, I will attempt to show how you could go about inventing the answer yourself. I will start by assuming you have already grokked the standard tricks for implementing reverse on plain lists, and show how with simple, local transformations you can turn that implementation into one that works on non-empty lists.

Here's the standard implementation of reverse for plain lists:

reverse xs = go [] xs where
    go as [] = as
    go as (b:bs) = go (b:as) bs

We'd like to modify this so that our recursive function is only called on non-empty lists (i.e. plain old lists that we know are not empty, not NonEmptys). So everywhere we call go, just before we do the call, we'll do a pattern match. If we would have called go on an empty list, we'll just inline what go would have done so that we don't have to call it. For example, instead of go [] xs, we'll do a case on xs to check whether it's empty and only call go when it isn't, like this:

reverse [] = [] -- inline the call go [] []
reverse (x:xs) = go [] (x:xs) {- no inlining, do the same call we would usually do -} where
    go as [] = as -- no change at all from before, since there's no recursive call
    go as (b:[]) = b:as -- inline the call go (b:as) []
    go as (b:b':bs) = go (b:as) (b':bs) -- no inlining, do the same call we would usually do

Now that we know go's argument is always non-empty, we can remove that case from the definition of go. All other cases remain exactly the same.

reverse [] = []
reverse (x:xs) = go [] (x:xs) where
    go as (b:[]) = b:as
    go as (b:b':bs) = go (b:as) (b':bs)

Now it's a bit odd that we're pattern-matching on the second argument to go to extract the first element of the list given that we know all callers have just done the same pattern match already. So let's just change the calling convention a little: callers will pass the already-matched first list element separately. Like this:

reverse [] = []
reverse (x:xs) = go [] x xs where
    go as b [] = b:as
    go as b (b':bs) = go (b:as) b' bs

Given this definition, it is now extremely easy to implement weirdReverse :: NonEmpty a -> [a]: simply ignore the first defining clause for empty inputs, since we know they don't exist! Of course we also have to change the : to a :| in the outer pattern match.

weirdReverse :: NonEmpty a -> [a]
weirdReverse (x:|xs) {- swap out constructors -} = go [] x xs where
    go as b [] = b:as
    go as b (b':bs) = go (b:as) b' bs

Finally, since go now clearly syntactically always returns a list with at least one element, it's easy to swap out the : in its return value for a :| to return a NonEmpty instead of a [].

neReverse :: NonEmpty a -> NonEmpty a
neReverse (x:|xs) = go [] x xs where
    go as b [] = b:|as -- swap out constructors
    go as b (b':bs) = go (b:as) b' bs

This pattern -- of implementing NonEmpty functions in essentially the same way as their [] counterparts, but "unrolling" each definition one layer and adding an argument for the known head -- is quite general; it should work well for things like map, foldr, zip, last, init, and so forth. I encourage you to give it a shot, as it's excellent exercise! If you want to start with a particularly similar one, try last.

huangapple
  • 本文由 发表于 2023年6月15日 17:55:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76481316.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定