在 haskell 中使用替代前奏曲

Using alternative preludes in haskell

我对其他 Preludes 很感兴趣。我知道有很多选择:

  1. https://hackage.haskell.org/packages/#cat:Prelude
  2. https://guide.aelve.com/haskell/alternative-preludes-zr69k1hc

我知道他们中的很多人修复的一个简单的事情是文本,另一个是像 head 这样的函数,当你可能更喜欢它们更安全时,这个错误很难解决。

但是,当我尝试使用这些替代方案时,head 中的行为,嗯,似乎完全破坏了功能,对我来说看起来不像是改进。以下是一些示例:

序曲

Prelude> head [1]
1
Prelude> head []
*** Exception: Prelude.head: empty list

基金会

Foundation> head [1]

<interactive>:6:6: error:
    • Couldn't match expected type ‘NonEmpty c’
                  with actual type ‘[Integer]’
    • In the first argument of ‘head’, namely ‘[1]’
      In the expression: head [1]
      In an equation for ‘it’: it = head [1]
    • Relevant bindings include
        it :: foundation-0.0.21:Foundation.Collection.Element.Element c
          (bound at <interactive>:6:1)
Foundation> head []

<interactive>:7:6: error:
    • Couldn't match expected type ‘NonEmpty c’ with actual type ‘[a0]’
    • In the first argument of ‘head’, namely ‘[]’
      In the expression: head []
      In an equation for ‘it’: it = head []
    • Relevant bindings include
        it :: foundation-0.0.21:Foundation.Collection.Element.Element c
          (bound at <interactive>:7:1)

安全

Safe> head []

<interactive>:22:1: error: Variable not in scope: head :: [a0] -> t

优雅的序曲

ClassyPrelude> head [1]

<interactive>:24:6: error:
    • Couldn't match expected type ‘NonNull mono’
                  with actual type ‘[Integer]’
    • In the first argument of ‘head’, namely ‘[1]’
      In the expression: head [1]
      In an equation for ‘it’: it = head [1]
    • Relevant bindings include
        it :: Element mono (bound at <interactive>:24:1)

回避

Relude> head [1]

<interactive>:27:6: error:
    • Couldn't match expected type ‘NonEmpty a’
                  with actual type ‘[Integer]’
    • In the first argument of ‘head’, namely ‘[1]’
      In the expression: head [1]
      In an equation for ‘it’: it = head [1]
    • Relevant bindings include it :: a (bound at <interactive>:27:1)

里约

RIO> head [1]

<interactive>:7:1: error:
    Variable not in scope: head :: [Integer] -> t

序曲

Protolude> head [1]
Just 1
Protolude> head []
Nothing

这看起来不错——它也适用于尾部,对吧?

Protolude> tail [1]

<interactive>:12:1: error:
    • Variable not in scope: tail :: [Integer] -> t
    • Perhaps you meant ‘tails’ (imported from Protolude)

Protolude> tails [1]
[[1],[]]

Protolude> tails []
[[]]

嗯,这不完全是直接替代品。

为什么这更好,为什么定义了这些函数,如果它们即将失败,我还缺少什么?

在大多数情况下,引入它们是因为它们在编译时而不是运行时失败。

Prelude.head 的问题不仅仅在于它会失败。这是它 必须 ,因为无法获取列表 [a] 并始终生成元素 a,因为输入列表可能为空。没有简单的替代品可以解决,需要彻底改变。

一个更安全、可以说更好的前奏可以通过以下方式之一解决这个问题:

  • 删除head,这样程序员就不会使用危险的工具了。 head 的任何使用都会在编译时失败。不太好,但还可以。

  • 限制输入类型,例如head :: NonEmptyList a -> a。这将是可用的,但程序员必须调整代码以保证输入列表确实是非空的。仅仅传递一个非空列表对编译器来说是行不通的——编译器需要一个 proof,这是正确的。 消息是以前的代码将充满编译错误,这将帮助程序员找出程序中需要修复的部分。

  • 限制输出类型,例如head :: [a] -> Maybe a。这可以很好地使用,但程序员需要处理不同的结果类型,并处理所有潜在的 Nothing。同样,编译时错误将帮助程序员确定需要修复的地方。

无论如何,程序员都得修改代码。没有办法解决它。但是,一旦编译时错误得到解决,程序将保证在运行时永远不会产生 head: empty list 错误。

我是 relude 作者之一,我可以提供一点动机,说明为什么 reludeheadtail、[=18 选择此行为=] 和 init 函数。

标准 Prelude 按以下方式定义 head

head :: [a] -> a

另类前奏通常定义head如下:

head :: [a] -> Maybe a

但是,relude 使用以下类型签名来实现它:

head :: NonEmpty a -> a

这个设计决定使库对初学者不太友好(人们可能不期望这种类型的 head 函数),但另一方面它使接口更加类型安全。

另一个原因:如果你有类型 head :: [a] -> Maybe a 的函数,你不能使用你的 head 的 Maybeised 版本来表达 head :: NonEmpty a -> a。但是,如果您有 headNonEmpty 一起使用,那么实现 head 和 returns Maybe a 就很容易了。 relude 甚至具有 viaNonEmpty 函数:

viaNonEmpty :: (NonEmpty a -> b) -> ([a] -> Maybe b)

在此处查看带有示例的文档: