在许多编程语言中都有一个“传染性函数标签”的常见概念——函数的一些特殊行为也必须扩展到其调用者。
- Rust 函数可以是
unsafe,这意味着它们执行的操作可能会违反内存不安全性。 unsafe 函数可以调用普通函数,但任何调用unsafe 函数的函数也必须是unsafe。
- Python 函数可以是
async,这意味着它们返回一个承诺而不是实际值。 async 函数可以调用普通函数,但 async 函数的调用(通过 await)只能由另一个 async 函数调用。
- Haskell 函数可以是不纯的,这意味着它们返回
IO a 而不是a。不纯函数可以调用纯函数,但不纯函数只能被其他不纯函数调用。
- 数学函数可以是部分,这意味着它们不会将域中的每个值都映射到输出。偏函数的定义可以引用全函数,但如果全函数将其部分域映射到偏函数,它也会变成偏函数。
虽然可能有一些方法可以从未标记函数调用标记函数,但没有通用方法,这样做通常很危险,并有可能破坏语言试图提供的抽象。
因此,拥有标签的好处是,您可以公开一组特殊的原语,这些原语被赋予了这个标签,并且任何使用这些原语的函数在其签名中都清楚地表明了这一点。
假设您是一名语言设计师并且您认识到这种模式,并且您决定要允许用户定义的标签。假设用户定义了一个标签Err,表示可能引发错误的计算。使用Err 的函数可能如下所示:
function div <Err> (n: Int, d: Int): Int
if d == 0
throwError("division by 0")
else
return (n / d)
如果我们想简化事情,我们可能会观察到接受参数并没有错误 - 它正在计算可能出现问题的返回值。所以我们可以将标签限制为不带参数的函数,并让div 返回一个闭包而不是实际值:
function div(n: Int, d: Int): <Err> () -> Int
() =>
if d == 0
throwError("division by 0")
else
return (n / d)
在 Haskell 等惰性语言中,我们不需要闭包,直接返回一个惰性值即可:
div :: Int -> Int -> Err Int
div _ 0 = throwError "division by 0"
div n d = return $ n / d
现在很明显,在 Haskell 中,标签不需要特殊的语言支持——它们是普通的类型构造函数。让我们为它们创建一个类型类!
class Tag m where
我们希望能够从标记函数调用未标记函数,这相当于将未标记值 (a) 转换为标记值 (m a)。
addTag :: a -> m a
我们还希望能够获取标记值 (m a) 并应用标记函数 (a -> m b) 来获得标记结果 (m b):
embed :: m a -> (a -> m b) -> m b
当然,这正是 monad 的定义! addTag对应return,embed对应(>>=)。
现在很明显,“标记函数”只是一种 monad。因此,每当您发现可以应用“功能标签”的地方时,您就有可能找到适合 monad 的地方。
附:关于我在这个答案中提到的标签:Haskell 用IO monad 模拟杂质,用Maybe monad 模拟偏心。大多数语言都相当透明地实现 async/promises,并且似乎有一个名为 promise 的 Haskell 包模仿了这个功能。 Err monad 等价于 Either String monad。我不知道有任何语言可以单元地模拟内存不安全性,这是可以做到的。