Dependent if expressions without dependent types
Dependent if expressions without dependent types
Haskell for all | Wednesday, September 2, 2026
This post showcases a folklore trick for implementing code that seems like it should require dependent types without actually requiring dependent types. In fact, this trick works in any language with simple Hindley-Milner type inference. 这篇文章展示了一个民间技巧,用于实现那些看起来需要依赖类型(dependent types)才能完成的代码,而实际上并不需要依赖类型。事实上,这个技巧适用于任何具有简单 Hindley-Milner 类型推断的语言。
To prove that, by the end of this post I’m going to show how you can make this Haskell code type-check and work: 为了证明这一点,在本文结束时,我将展示如何让这段 Haskell 代码通过类型检查并正常运行:
example bool = if bool then 5 else "hi!"
main = do
print (example false) -- "hi!"
print (example true ) -- 5
print (example (false && true)) -- "hi!"
print (example (false || true)) -- 5
print (example (not true)) -- "hi!"
… and it will only require one language extension: RebindableSyntax. I picked up this trick from studying various Haskell packages, most notably the formatting package. Here I’m adapting the trick to modeling dependent if expressions which can return different types based on their input. I’ll build up to the trick in two steps: first I’ll explain a related trick (Church-encoded booleans) and then generalize that trick to implement our dependent if expressions.
……而且它只需要一个语言扩展:RebindableSyntax。我是在研究各种 Haskell 包(最著名的是 formatting 包)时学到这个技巧的。在这里,我将该技巧应用于模拟依赖 if 表达式,这些表达式可以根据输入返回不同的类型。我将分两步来构建这个技巧:首先解释一个相关的技巧(Church 编码布尔值),然后推广该技巧以实现我们的依赖 if 表达式。
Church encoding
Church 编码
“Church encoding” is a technique for encoding data structures and operations on those data structures using pure functions and nothing else. For example, we can Church-encode boolean values as functions that accept two arguments and return one of the two arguments as their result: “Church 编码”是一种仅使用纯函数来编码数据结构及其操作的技术。例如,我们可以将布尔值 Church 编码为接受两个参数并返回其中一个作为结果的函数:
{-# LANGUAGE RankNTypes #-}
import Prelude hiding (Bool(..), not, (&&), (||))
type Bool = forall a . a -> a -> a
true :: Bool
true thenBranch elseBranch = thenBranch
false :: Bool
false thenBranch elseBranch = elseBranch
The reason I name the function arguments thenBranch and elseBranch is because you can think of these Church-encoded boolean values as “pre-formed if expressions”, meaning that the two function arguments (thenBranch and elseBranch) represent the then and else branches of an if expression and the boolean value selects which branch to return as the result.
我将函数参数命名为 thenBranch 和 elseBranch 的原因是,你可以将这些 Church 编码的布尔值视为“预先形成的 if 表达式”,这意味着这两个函数参数(thenBranch 和 elseBranch)代表了 if 表达式的 then 和 else 分支,而布尔值则决定了返回哪个分支作为结果。
In fact, we can define an ifThenElse function that behaves just like an if expression except that it expects the condition to be a Church-encoded boolean value:
事实上,我们可以定义一个 ifThenElse 函数,它的行为与 if 表达式完全一样,只是它要求条件必须是一个 Church 编码的布尔值:
ifThenElse :: Bool -> a -> a -> a
ifThenElse condition thenBranch elseBranch = condition thenBranch elseBranch
… and it works just like an if expression would:
……它的工作方式就像 if 表达式一样:
>>> ifThenElse true "then branch" "else branch"
"then branch"
>>> ifThenElse false "then branch" "else branch"
"else branch"
NOTE: If you want to follow along and/or run any of these examples you can find the complete code in the Appendix. 注意:如果你想跟随本文操作或运行这些示例,可以在附录中找到完整代码。
To see why the above code works, let’s reason through what happens if we invoke our ifThenElse function on true:
为了理解上述代码为何有效,让我们推导一下当我们对 true 调用 ifThenElse 函数时会发生什么:
ifThenElse true thenBranch elseBranch
-- According to the definition of `ifThenElse`:
= true thenBranch elseBranch
-- According to the definition of `true`:
= thenBranch
The above expression returns the thenBranch, just like a traditional if expression would when given true. Similarly, if we invoke ifThenElse on false then we get the elseBranch:
上述表达式返回了 thenBranch,就像传统的 if 表达式在给定 true 时所做的那样。同样,如果我们对 false 调用 ifThenElse,我们就会得到 elseBranch:
ifThenElse false thenBranch elseBranch
= false thenBranch elseBranch
= elseBranch
In fact, we can go a step further and change Haskell’s if/then/else syntax to use the above ifThenElse function. If we enable the RebindableSyntax language extension then all expressions of the form if condition then thenBranch else elseBranch are desugared to ifThenElse condition thenBranch elseBranch using whatever ifThenElse function happens to be in scope:
事实上,我们可以更进一步,改变 Haskell 的 if/then/else 语法以使用上述 ifThenElse 函数。如果我们启用 RebindableSyntax 语言扩展,那么所有形式为 if condition then thenBranch else elseBranch 的表达式都会被脱糖(desugar)为 ifThenElse condition thenBranch elseBranch,并使用当前作用域内的任何 ifThenElse 函数:
{-# LANGUAGE RebindableSyntax #-}
toString :: Bool -> String
toString bool = if bool then "true" else "false"
main :: IO ()
main = do
print (toString false) -- "false"
print (toString true ) -- "true"
We can reason through why this works the same way as before: 我们可以推导为什么这与之前的工作方式相同:
toString false
-- According to the definition of `toString`:
= if false then "true" else "false"
-- `if`/`then`/`else` desugars to `ifThenElse`
= ifThenElse false "true" "false"
-- According to the definition of `ifThenElse`:
= false "true" "false"
-- According to the definition of `false`:
= "false"
We can keep going, though, and implement all the usual boolean operations to work on these Church-encoded boolean values: 不过,我们可以继续实现所有常见的布尔运算,以作用于这些 Church 编码的布尔值:
not :: Bool -> Bool
not bool thenBranch elseBranch = bool elseBranch thenBranch
(&&) :: Bool -> Bool -> Bool
(x && y) thenBranch elseBranch = x (y thenBranch elseBranch) elseBranch
(||) :: Bool -> Bool -> Bool
(x || y) thenBranch elseBranch = x thenBranch (y thenBranch elseBranch)
… and they work exactly the way we expect: ……它们的工作方式完全符合我们的预期:
main :: IO ()
main = do
print (toString (not true)) -- "false"
print (toString (false && true)) -- "false"
print (toString (false || true)) -- "true"
… and we can reason through that last example like this: ……我们可以像这样推导最后一个例子:
toString (false || true)
= if false || true then "true" else "false"
= ifThenElse (false || true) "true" "false"
= (false || true) "true" "false"
= false "true" (true "true" "false")
= true "true" "false"
= "true"
However, the original motivating example function still does not type-check with these Church-encoded booleans. If we try to type-check it we get: 然而,最初的激励示例函数在使用这些 Church 编码的布尔值时仍然无法通过类型检查。如果我们尝试进行类型检查,会得到:
ghci> example bool = if bool then 5 else "hi!"
<interactive>:2:29: error: [GHC-39999]
• No instance for ‘Num String’ arising from the literal ‘5’
• In the expression: 5
In the expression: if bool then 5 else "hi!"
In an equation for ‘example’: example bool = if bool then 5 else "hi!"
So how are we going to make this work? 那么我们该如何让它工作呢?
The trick
这个技巧
So far we had to enable two language extensions to make these Church-encoded booleans work: RankNTypes, RebindableSyntax … but earlier I said that we were going to make dependent if expressions work with only one extension: RebindableSyntax. So does this mean that we’re going to somehow make our Church-encoded booleans more powerful with fewer language extensions? Yes! In fact, all we have to do is delete all of the type declarations, type signatures, and the RankNTypes extension, like this:
到目前为止,我们必须启用两个语言扩展才能使这些 Church 编码的布尔值工作:RankNTypes 和 RebindableSyntax……但我之前说过,我们将只用一个扩展 RebindableSyntax 来实现依赖 if 表达式。那么,这是否意味着我们将以更少的语言扩展使 Church 编码的布尔值变得更强大?是的!事实上,我们所要做的就是删除所有的类型声明、类型签名和 RankNTypes 扩展,如下所示:
{-# LANGUAGE RebindableSyntax #-}
import Prelude hiding (Bool(..), not, (&&), (||))
true thenBranch elseBranch = ...