"error" 函数的存在如何影响 Haskell 的纯度?
How does the presence of the "error" function bear on the purity of Haskell?
我一直想知道 Haskell 异常系统是如何适应整个 "Pure functional language" 的。例如,请参阅下面的 GHCi 会话。
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Prelude> head []
*** Exception: Prelude.head: empty list
Prelude> :t head
head :: [a] -> a
Prelude> :t error
error :: [Char] -> a
Prelude> error "ranch"
*** Exception: ranch
CallStack (from HasCallStack):
error, called at <interactive>:4:1 in interactive:Ghci1
Prelude>
头的类型是[a] -> a。但是当你在空列表的特殊情况下调用它时,你会得到一个异常。但是类型签名中未考虑此异常。
如果我没记错的话,模式匹配失败的情况与此类似。类型签名说什么并不重要,如果您没有考虑到所有可能的模式,您 运行 就有抛出异常的风险。
我没有一个简洁的问题要问,但我的头在游泳。将这个奇怪的异常系统添加到原本纯净优雅的语言中的动机是什么?它仍然是纯净的,但我只是缺少一些东西吗?如果我想利用这个异常特性,我将如何着手去做(即我如何捕获和处理异常?还有什么我可以用它们做的吗?)例如,如果我编写代码使用"head" 函数,我当然应该采取预防措施,以防空列表不知何故走私进来。
For example, if ever I write code that uses the "head" function, surely I should take precautions for the case where an empty list somehow smuggles itself in.
更简单的解决方案:不要使用 head
。有很多替换:listToMaybe
来自 Data.Maybe
,safe package, etc. The partial functions [1] in the base libraries -- specially ones as easy to replace as head
-- are little more than historical cruft, and should be either ignored or replaced by safe variants, such as those in the aforementioned safe package. For further arguments, here is an entirely reasonable rant about partial functions.
中的各种替代实现
If I want to take advantage of this exception feature, how would I go about doing it (ie how do I catch and handle exceptions? is there anything else I can do with them?)
error
抛出的异常只能在 IO
monad 中捕获。如果你正在编写纯函数,你不会想强迫你的用户在 IO
monad 中 运行 它们只是为了捕获异常。因此,如果您曾经在纯函数中使用 error
,请假设错误不会被捕获 [2]。理想情况下,您根本不应该在纯代码中使用 error
,但如果您出于某种原因被迫这样做,至少要确保编写一条信息性错误消息(即 not "Prelude.head: empty list") 以便您的用户知道程序崩溃时发生了什么。
If I remember correctly it's a similar story when there is a failure during pattern matching. It doesn't matter what the type signature says, if you haven't accounted for every possible pattern, you run the risk of throwing an exception.
确实如此。使用 head
与自己显式编写不完整的模式匹配 (\(x:_) -> x)
的唯一区别是,在后一种情况下,如果您使用 -Wall
,编译器至少会警告您,而使用 head
即使是被扫到地毯下。
I've always wondered how the Haskell exception system fits in with the whole "Pure functional language" thing.
从技术上讲,部分函数不会影响纯度(当然,这不会使它们变得不那么讨厌)。从理论的角度来看,head []
与 foo = let x = x in x
一样是未定义的。 (进一步阅读这些微妙之处的关键字是 。)
[1]: 部分函数是像 head
一样的函数,它们没有为它们应该采用的参数类型的某些值定义。
[2]:值得一提的是 IO
中的异常是一个完全不同的问题,因为您无法轻易避免,例如仅通过使用更好的功能来读取文件失败。有很多方法可以以明智的方式处理此类情况。如果您对这个问题感到好奇,here is one "highly opinionated" article about it 这是对相关工具和权衡的说明。
您混淆了两个概念:纯度和总量。
- Purity 表示函数没有副作用。
- Totality 表示每个函数终止并产生一个值。
Haskell 是纯粹的,但不是总的。
在 IO
之外,非终止项(例如 let loop = loop in loop
)和例外项(例如 error "urk!"
)是相同的——非终止项和例外项在强制使用时不会计算为一个值。 Haskell 的设计者想要一种图灵完备的语言,根据停止问题,这意味着他们放弃了整体性。一旦你有非终止,我想你也可能有例外——定义 error msg = error msg
并调用 error
永远什么都不做在实践中比 actually在有限的时间内看到你想要的错误信息!
但总的来说,你是对的——部分函数(那些没有为每个输入值定义的函数,比如 head
)是丑陋的。现代 Haskell 通常更喜欢通过返回 Maybe
或 Either
值来编写总函数,例如
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
errHead :: [a] -> Either String a
errHead [] = Left "Prelude.head: empty list"
errHead (x:_) = Right x
在这种情况下,Functor
, Applicative
, Monad
, MonadError
, Foldable
, Traversable
等机器可以轻松组合这些总功能并轻松处理它们的结果。
如果您真的在代码中遇到异常 – 例如,您可能会使用 error
检查代码中您认为已强制执行但存在错误的复杂不变量 – 您可以在 IO
中捕捉到它。 returns 为什么可以与 IO
中的异常交互的问题 – 这不会使语言变得不纯吗?答案与为什么我们可以在 IO
中执行 I/O 或使用可变变量的问题相同——评估类型 IO A
的值不会产生副作用它描述的只是一个描述程序可以做什么的动作。 (互联网上其他地方对此有更好的描述;例外与其他效果没有任何不同。)
(另外,请注意有 a separate-but-related exception system in IO
,它在例如尝试读取不存在的文件时使用。人们通常对这个异常系统没有意见,但要适度,因为自从你在 IO
中,您已经在使用不纯代码。)
Haskell 不要求您的函数是完整的,并且在它们不是时不进行跟踪。 (总函数是那些对其输入类型的每个可能值都有明确定义的输出的函数)
即使没有异常或模式匹配失败,您也可以拥有一个不为某些输入定义输出的函数,它会一直运行下去。一个例子是 length (repeat 1)
。这将永远计算,但实际上永远不会抛出错误。
Haskell语义"copes"的方式是声明每个类型中都有一个"extra"值;所谓的“bottom value”,并声明任何未正确完成并生成其类型的正常值的计算实际上会生成底部值。它由数学符号 ⊥ 表示(仅在谈论 关于 Haskell 时;在 Haskell 中没有任何方法可以直接引用该值,但是 undefined
也经常被使用,因为 是 一个 Haskell 绑定到错误计算的名称,因此在语义上产生底部值)。
这个是系统中的理论上的缺陷,因为它使您能够创建任何类型的'value'(尽管不是很有用),许多关于基于类型的代码正确的推理实际上依赖于这样的假设,即你不能完全做到这一点(如果你进入 Curry-Howard 同构纯函数程序和形式逻辑,⊥ 的存在使您能够 "prove" 逻辑矛盾,从而证明任何事情。
但在实践中似乎可以证明,通过假装 ⊥ 在 Haskell 中不存在而进行的所有推理通常仍然有效,可以在您编写 "well-behaved" 时发挥作用不怎么用⊥的代码
在 Haskell 中容忍这种情况的主要原因是作为 编程语言 的易用性,而不是形式逻辑或数学系统。不可能制作出一个编译器,它实际上可以判断任意类似 Haskell 的代码,无论每个函数是完整的还是部分的(请参阅暂停问题)。因此,一种想要强制完整性的语言将不得不删除很多你可以做的事情,或者要求你跳过很多环节来证明你的代码总是终止,或者两者兼而有之。 Haskell 设计师不想那样做。
因此,考虑到 Haskell 作为一种语言已经接受了偏颇和 ⊥,它也可能会为您提供 error
之类的东西作为方便。毕竟,您总是可以通过不终止来编写 error :: String -> a
函数;立即打印出错误消息,而不是让程序永远旋转,这对练习程序员有用得多,即使它们在 Haskell 语义理论中是等价的!
同样,Haskell 的最初设计者决定,隐式地向每个只是出错的模式匹配添加一个包罗万象的案例,这比强迫程序员在每次他们期望时显式地添加错误案例更方便他们的一部分代码只能看到某些情况。 (虽然很多 Haskell 程序员,包括我在内,都会处理不完整模式匹配警告,并且几乎总是将其视为错误并修复他们的代码,因此可能更喜欢最初的 Haskell 设计师在这个上走了另一条路)。
TLDR; error
和模式匹配失败的异常是为了方便,因为它们不会使系统比它已经必须的更崩溃,而不是与 Haskell.[=23 完全不同的系统=]
你 可以 通过抛出和捕获异常来编程,如果你真的想要的话,包括捕获来自 error
的异常或模式匹配失败,通过使用来自 [= 的工具22=].
为了不破坏系统的纯洁性,您可以从任何地方引发异常(因为系统总是必须处理函数未正确终止和生成的可能性一个值;"raising an exception" 只是另一种可能发生的方式),但异常只能由 IO
中的构造捕获。因为 IO
的形式语义基本上允许任何事情发生(因为它必须与现实世界交互,并且我们可以根据 Haskell 的定义对其施加任何硬性限制),我们还可以放宽 Haskell 中纯函数通常需要的大部分规则,并且仍然有一些技术上适合 Haskell 纯代码模型的东西。
我根本没怎么用过它(通常我更喜欢使用在 Haskell 的语义模型方面比操作模型定义更明确的东西来保持我的错误处理IO
可以,可以像 Maybe
或 Either
) 一样简单,但如果需要,您可以阅读它。
我一直想知道 Haskell 异常系统是如何适应整个 "Pure functional language" 的。例如,请参阅下面的 GHCi 会话。
GHCi, version 8.0.1: http://www.haskell.org/ghc/ :? for help
Prelude> head []
*** Exception: Prelude.head: empty list
Prelude> :t head
head :: [a] -> a
Prelude> :t error
error :: [Char] -> a
Prelude> error "ranch"
*** Exception: ranch
CallStack (from HasCallStack):
error, called at <interactive>:4:1 in interactive:Ghci1
Prelude>
头的类型是[a] -> a。但是当你在空列表的特殊情况下调用它时,你会得到一个异常。但是类型签名中未考虑此异常。
如果我没记错的话,模式匹配失败的情况与此类似。类型签名说什么并不重要,如果您没有考虑到所有可能的模式,您 运行 就有抛出异常的风险。
我没有一个简洁的问题要问,但我的头在游泳。将这个奇怪的异常系统添加到原本纯净优雅的语言中的动机是什么?它仍然是纯净的,但我只是缺少一些东西吗?如果我想利用这个异常特性,我将如何着手去做(即我如何捕获和处理异常?还有什么我可以用它们做的吗?)例如,如果我编写代码使用"head" 函数,我当然应该采取预防措施,以防空列表不知何故走私进来。
For example, if ever I write code that uses the "head" function, surely I should take precautions for the case where an empty list somehow smuggles itself in.
更简单的解决方案:不要使用 head
。有很多替换:listToMaybe
来自 Data.Maybe
,safe package, etc. The partial functions [1] in the base libraries -- specially ones as easy to replace as head
-- are little more than historical cruft, and should be either ignored or replaced by safe variants, such as those in the aforementioned safe package. For further arguments, here is an entirely reasonable rant about partial functions.
If I want to take advantage of this exception feature, how would I go about doing it (ie how do I catch and handle exceptions? is there anything else I can do with them?)
error
抛出的异常只能在 IO
monad 中捕获。如果你正在编写纯函数,你不会想强迫你的用户在 IO
monad 中 运行 它们只是为了捕获异常。因此,如果您曾经在纯函数中使用 error
,请假设错误不会被捕获 [2]。理想情况下,您根本不应该在纯代码中使用 error
,但如果您出于某种原因被迫这样做,至少要确保编写一条信息性错误消息(即 not "Prelude.head: empty list") 以便您的用户知道程序崩溃时发生了什么。
If I remember correctly it's a similar story when there is a failure during pattern matching. It doesn't matter what the type signature says, if you haven't accounted for every possible pattern, you run the risk of throwing an exception.
确实如此。使用 head
与自己显式编写不完整的模式匹配 (\(x:_) -> x)
的唯一区别是,在后一种情况下,如果您使用 -Wall
,编译器至少会警告您,而使用 head
即使是被扫到地毯下。
I've always wondered how the Haskell exception system fits in with the whole "Pure functional language" thing.
从技术上讲,部分函数不会影响纯度(当然,这不会使它们变得不那么讨厌)。从理论的角度来看,head []
与 foo = let x = x in x
一样是未定义的。 (进一步阅读这些微妙之处的关键字是
[1]: 部分函数是像 head
一样的函数,它们没有为它们应该采用的参数类型的某些值定义。
[2]:值得一提的是 IO
中的异常是一个完全不同的问题,因为您无法轻易避免,例如仅通过使用更好的功能来读取文件失败。有很多方法可以以明智的方式处理此类情况。如果您对这个问题感到好奇,here is one "highly opinionated" article about it 这是对相关工具和权衡的说明。
您混淆了两个概念:纯度和总量。
- Purity 表示函数没有副作用。
- Totality 表示每个函数终止并产生一个值。
Haskell 是纯粹的,但不是总的。
在 IO
之外,非终止项(例如 let loop = loop in loop
)和例外项(例如 error "urk!"
)是相同的——非终止项和例外项在强制使用时不会计算为一个值。 Haskell 的设计者想要一种图灵完备的语言,根据停止问题,这意味着他们放弃了整体性。一旦你有非终止,我想你也可能有例外——定义 error msg = error msg
并调用 error
永远什么都不做在实践中比 actually在有限的时间内看到你想要的错误信息!
但总的来说,你是对的——部分函数(那些没有为每个输入值定义的函数,比如 head
)是丑陋的。现代 Haskell 通常更喜欢通过返回 Maybe
或 Either
值来编写总函数,例如
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
errHead :: [a] -> Either String a
errHead [] = Left "Prelude.head: empty list"
errHead (x:_) = Right x
在这种情况下,Functor
, Applicative
, Monad
, MonadError
, Foldable
, Traversable
等机器可以轻松组合这些总功能并轻松处理它们的结果。
如果您真的在代码中遇到异常 – 例如,您可能会使用 error
检查代码中您认为已强制执行但存在错误的复杂不变量 – 您可以在 IO
中捕捉到它。 returns 为什么可以与 IO
中的异常交互的问题 – 这不会使语言变得不纯吗?答案与为什么我们可以在 IO
中执行 I/O 或使用可变变量的问题相同——评估类型 IO A
的值不会产生副作用它描述的只是一个描述程序可以做什么的动作。 (互联网上其他地方对此有更好的描述;例外与其他效果没有任何不同。)
(另外,请注意有 a separate-but-related exception system in IO
,它在例如尝试读取不存在的文件时使用。人们通常对这个异常系统没有意见,但要适度,因为自从你在 IO
中,您已经在使用不纯代码。)
Haskell 不要求您的函数是完整的,并且在它们不是时不进行跟踪。 (总函数是那些对其输入类型的每个可能值都有明确定义的输出的函数)
即使没有异常或模式匹配失败,您也可以拥有一个不为某些输入定义输出的函数,它会一直运行下去。一个例子是 length (repeat 1)
。这将永远计算,但实际上永远不会抛出错误。
Haskell语义"copes"的方式是声明每个类型中都有一个"extra"值;所谓的“bottom value”,并声明任何未正确完成并生成其类型的正常值的计算实际上会生成底部值。它由数学符号 ⊥ 表示(仅在谈论 关于 Haskell 时;在 Haskell 中没有任何方法可以直接引用该值,但是 undefined
也经常被使用,因为 是 一个 Haskell 绑定到错误计算的名称,因此在语义上产生底部值)。
这个是系统中的理论上的缺陷,因为它使您能够创建任何类型的'value'(尽管不是很有用),许多关于基于类型的代码正确的推理实际上依赖于这样的假设,即你不能完全做到这一点(如果你进入 Curry-Howard 同构纯函数程序和形式逻辑,⊥ 的存在使您能够 "prove" 逻辑矛盾,从而证明任何事情。
但在实践中似乎可以证明,通过假装 ⊥ 在 Haskell 中不存在而进行的所有推理通常仍然有效,可以在您编写 "well-behaved" 时发挥作用不怎么用⊥的代码
在 Haskell 中容忍这种情况的主要原因是作为 编程语言 的易用性,而不是形式逻辑或数学系统。不可能制作出一个编译器,它实际上可以判断任意类似 Haskell 的代码,无论每个函数是完整的还是部分的(请参阅暂停问题)。因此,一种想要强制完整性的语言将不得不删除很多你可以做的事情,或者要求你跳过很多环节来证明你的代码总是终止,或者两者兼而有之。 Haskell 设计师不想那样做。
因此,考虑到 Haskell 作为一种语言已经接受了偏颇和 ⊥,它也可能会为您提供 error
之类的东西作为方便。毕竟,您总是可以通过不终止来编写 error :: String -> a
函数;立即打印出错误消息,而不是让程序永远旋转,这对练习程序员有用得多,即使它们在 Haskell 语义理论中是等价的!
同样,Haskell 的最初设计者决定,隐式地向每个只是出错的模式匹配添加一个包罗万象的案例,这比强迫程序员在每次他们期望时显式地添加错误案例更方便他们的一部分代码只能看到某些情况。 (虽然很多 Haskell 程序员,包括我在内,都会处理不完整模式匹配警告,并且几乎总是将其视为错误并修复他们的代码,因此可能更喜欢最初的 Haskell 设计师在这个上走了另一条路)。
TLDR; error
和模式匹配失败的异常是为了方便,因为它们不会使系统比它已经必须的更崩溃,而不是与 Haskell.[=23 完全不同的系统=]
你 可以 通过抛出和捕获异常来编程,如果你真的想要的话,包括捕获来自 error
的异常或模式匹配失败,通过使用来自 [= 的工具22=].
为了不破坏系统的纯洁性,您可以从任何地方引发异常(因为系统总是必须处理函数未正确终止和生成的可能性一个值;"raising an exception" 只是另一种可能发生的方式),但异常只能由 IO
中的构造捕获。因为 IO
的形式语义基本上允许任何事情发生(因为它必须与现实世界交互,并且我们可以根据 Haskell 的定义对其施加任何硬性限制),我们还可以放宽 Haskell 中纯函数通常需要的大部分规则,并且仍然有一些技术上适合 Haskell 纯代码模型的东西。
我根本没怎么用过它(通常我更喜欢使用在 Haskell 的语义模型方面比操作模型定义更明确的东西来保持我的错误处理IO
可以,可以像 Maybe
或 Either
) 一样简单,但如果需要,您可以阅读它。