lambda 演算和函数式编程中的异常处理

Exception handling in lambda calculus and functional programming

提问人:Otávio Augusto Silva 提问时间:4/27/2023 更新时间:4/27/2023 访问量:142

问:

有没有办法在 lambda 演算中对异常处理进行建模?
我之所以这么问,是因为在过程语言和衍生范式中处理异常状态的多种方式是很常见的。即使在 C 语言中,您也可以简单地使用 、 和 来模拟这种行为。
我可以在脑海中想象图灵机状态图中的一种方式,一种可以被任何其他节点访问的“异常”节点,我猜过程语言会做这种事情来实现它。
但是在Haskell(我最近的痴迷)和一般的函数式编程中,你看不到所有关于异常的模糊。我知道它们存在(?),我知道函数式编程使用单子来模拟副作用,但我承认我不太明白它是什么以及它是如何工作的。
但是我在另一个讨论中看到,所有函数式语言都是 lambda 演算的句法糖,那么这样的事情是如何工作的呢?
setjmp.herrno.hsignal.hControl.Exception

异常 Haskell 函数规划 理论 lambda-calculus

评论

1赞 coredump 4/27/2023
异常处理可以在分隔的延续之上实现,这可以通过使用延续传递样式在 lambda 演算中实现?

答:

8赞 Silvio Mayolo 4/27/2023 #1

首先注意:如果您刚开始使用 Haskell,请远离 .该模块在 monad 中模拟传统的 Java 样式异常处理。最终学习这是一件好事,但它不是在纯功能Haskell代码中处理(可控)错误的传统方法。Control.ExceptionIO

正如你已经注意到的,在像 Haskell 这样的函数式语言中处理特殊情况的常用方法是使用 monads。因此,让我们谈谈单子,但不是抽象地谈论,而是让我们谈谈一个具体的单子。这种类型存在于标准库中,但我用另一个更令人回味的名字来称呼它。

data Result e a = Err e | Ok a

我们使用这种类型的方法很简单。每当我们有一个可以“抛出”异常的函数时,该函数都会返回一个 ,如果一切顺利,则为结果类型,是我们抛出的异常类型。这有点像 Java 中检查的异常,但规模要细得多。Result e aae

作为一个可能很典型的例子,考虑一个函数,它除以两个数字,但如果我们试图除以零,就会出错。它的签名是这样的。Result

-- Singleton error type
data DivideByZero = DivideByZero

divide :: Double -> Double -> Result DivideByZero Double
divide _ 0 = Err DivideByZero
divide x y = Ok (x / y) -- Built-in Haskell division

因此,如果我们发现自己处于 ,即 类型的值 ,但需要注意的是,我们可能已经失败了,并且该对象“包含”了 类型的异常。Result e aaResulte

现在让我们看看实例会给我们带来什么操作。单子的层次结构从 开始,然后向下移动到 ,最后是 。 给我们MonadFunctorApplicativeMonadFunctor

fmap :: (a -> b) -> Result e a -> Result e b

我们可以将其理解为“如果我有一条路要从到(不能失败),那么我总是可以采取一个(潜在失败)并产生一个(潜在失败)”。这是有道理的,我们可以很容易地写出来。abab

fmap f (Ok a) = Ok (f a)
fmap _ (Err e) = Err e

我们只是保留错误状态。如果计算已经出错,我们会保留错误。如果没有,我们应用我们的 (完全安全,纯净的)功能 .f

现在出现了一个自然的问题:如果我有两个参数的函数,但我有 和 怎么办?我们可以尝试讨好函数,但是一旦我们应用了第一个参数,我们就遇到了问题。f :: a -> b -> cx :: Result e ay :: Result e b

fmap f x :: Result e (b -> c)
y :: Result e b

现在我想将一个可能失败的函数应用于一个可能失败的值。换句话说,我有两个潜在的错误对象,我想将它们组合在一起(在本例中,使用函数应用程序)。这就是用武之地。Applicative

(<*>) :: Result e (a -> b) -> Result e a -> Result e b

运算符只是使用一个已经存在于我们应用程序内部的函数(在本例中,一个可能秘密地成为异常的函数)。Applicative(<*>)fmap

以下是它的实现方式。

Err e <*> _ = Err e
Ok _ <*> Err e = Err e
Ok f <*> Ok x = Ok (f x)

现在我们可以将示例函数应用为

fmap f x <*> y :: Result e c

此外,还为我们提供了与我们的类型一样实现的。这使我们能够将纯值视为可能失败的值,从根本上“忘记”它不会失败的事实。当您尝试使类型签名对齐时,它会派上用场,例如,如果您有一个函数,该函数预计计算可能会失败,但您所拥有的只是一个纯函数。Applicativepure :: a -> Result e apure = Ok

最后,我们得到. 只需要实现一个函数,那就是 bind 运算符。MonadMonad(>>=)

(>>=) :: Result e a -> (a -> Result e b) -> Result e b

好吧,这看起来很不一样。让我们翻转一下论点。

fmap       ::          (a -> b) -> Result e a -> Result e b
(<*>)      :: Result e (a -> b) -> Result e a -> Result e b
flip (>>=) :: (a -> Result e b) -> Result e a -> Result e b

我们刚刚将函数的一部分移动了一点。现在,不仅我们的函数可能失败,而且我们的函数可以根据我们前面计算的输入值选择是否失败。Resulta

这非常强大,因为它允许我们用命令式语言实现我们对异常所做的最常见的事情:传播它们。

首先,让我们看看它是如何实现的。

Err e >>= _ = Err e
Ok x >>= f = f x

现在,每当我们调用一个函数并希望简单地让异常传播到我们自己的调用者时,我们都会使用函数的结果。>>=

myComplicatedComputation :: Int -> Int -> Int -> Result ExceptionType Bool
myComplicatedComputation a b c =
  someMath a b >>= (\tmp ->
    someMoreMath tmp c >>= (\result ->
      pure (result > 0)))

在 Java 中,我们将其写为

public static boolean myComplicatedComputation(int a, int b, int c) throws ExceptionType {
  int tmp = someMath(a, b);
  int result = someMoreMath(tmp, c);
  return (result > 0);
}

如果我们能像这样简洁地编写我们的 Haskell 代码,让我们几乎忘记我们正在处理效果,那肯定是件好事。让我稍微缩进一下,并删除一些不必要的括号。

myComplicatedComputation :: Int -> Int -> Int -> Result ExceptionType Bool
myComplicatedComputation a b c =
  someMath a b >>= \tmp ->
  someMoreMath tmp c >>= \result ->
  pure (result > 0)

引入 do 表示法。重要的是要记住,符号是纯粹的语法糖。在任何时候,它都不会为语言添加任何新内容。它完全是根据其他操作实现的。这与上述完全相同。do(>>=)Monad

myComplicatedComputation :: Int -> Int -> Int -> Result ExceptionType Bool
myComplicatedComputation a b c = do
  tmp <- someMath a b
  result <- someMoreMath tmp c
  pure $ result > 0

所以这一切都是在功能方面实现的。我们没有“应用”一个函数,而是使用这种有趣的类似函数应用程序的运算符,称为“bind”并编写。俗话说,它的功能一直向下。(>>=)

值得注意的是,这正是 Rust 所做的。Rust 采用一元方法处理自己的类型。 是 , 是 , 是 和 是 。(没有直接等价物,但可以很容易地实现)而 Rust 中的运算符,基本上意味着“将错误传播给我的调用者”,实际上只是如果你有错误时短路的捷径。这是 Rust 对 Haskell 符号的回答。Result<T, E>fmap.mappureOk(>>=).and_then(<*>).and_then?do

评论

3赞 Mark Seemann 4/27/2023
我想也许你本来想回到这个,但忘记了:像这样工作的现有内置类型是 .只是把它留给 OP 和其他读者。很好的答案!Either