Example
如果不重新实现 Haskell 的某些部分,并非每个值都可以在页面上表示。例如,return putStrLn
有一种类型Example (String -> IO ())
,但我认为尝试Example
从文件中解析出这种类型的值是没有意义的。
因此,让我们限制自己解析您给出的示例,这些示例仅包含对的调用foo
和bar
排序>>
(即,没有变量绑定和任意计算)*。我们语法的巴库斯-瑙尔形式大致如下:
<program> ::= "" | <expr> "\n" <program>
<expr> ::= "foo " <integer> | "bar " <string>
解析我们的两种表达式很简单......
type Parser = Parsec String ()
int :: Parser Int
int = fmap read (many1 digit)
parseFoo :: Parser (Example ())
parseFoo = string "foo " *> fmap foo int
parseBar :: Parser (Example Int)
parseBar = string "bar " *> fmap bar (many1 alphaNum)
...但是我们怎样才能给这两个解析器的组合一个类型呢?
parseExpr :: Parser (Example ???)
parseExpr = parseFoo <|> parseBar
parseFoo
并且parseBar
有不同的类型,所以我们不能用<|> :: Alternative f => f a -> f a -> f a
. 此外,没有办法提前知道我们给出的程序将是哪种类型:正如您所指出的,已解析程序的类型取决于输入字符串的值。“依赖于值的类型”称为依赖类型;Haskell 没有适当的依赖类型系统,但它已经足够接近我们可以尝试使这个示例工作。
让我们首先强制两边的表达式<|>
具有相同的类型。这涉及使用存在量化Example
擦除的类型参数。†</p>
data Ex a = forall i. Wrap (a i)
parseExpr :: Parser (Ex Example)
parseExpr = fmap Wrap parseFoo <|> fmap Wrap parseBar
此类型检查,但解析器现在返回一个Example
包含未知类型的值。未知类型的值当然是无用的——但我们确实知道一些关于Example
' 参数的信息:它必须是()
or ,因为它们是andInt
的返回类型。编程是将知识从你的大脑中提取出来并放到页面上,所以我们将用一些GADT证据来总结价值,当打开这些证据时,它会告诉你是还是.parseFoo
parseBar
Example
a
Int
()
data Ty a where
IntTy :: Ty Int
UnitTy :: Ty ()
data (a :*: b) i = a i :&: b i
type Sig a b = Ex (a :*: b)
pattern Sig x y = Wrap (x :&: y)
parseExpr :: Parser (Sig Ty Example)
parseExpr = fmap (\x -> Sig UnitTy x) parseFoo <|>
fmap (\x -> Sig IntTy x) parseBar
Ty
是(类似于)代表Example
's 类型参数的运行时“单例”。当您进行模式匹配时IntTy
,您会了解到a ~ Int
;当您进行模式匹配时,UnitTy
您会了解到这一点a ~ ()
。(信息可以以另一种方式流动,从类型到值,使用类。):*:
,仿函数产品,将两个类型构造函数配对,确保它们的参数相等;因此, 上的模式匹配会Ty
告诉您其伴随的Example
.
Sig
因此被称为依赖对或sigma类型 - 该对的第二个组件的类型取决于第一个组件的值。这是一种常见的技术:当您通过存在量化删除类型参数时,通常需要通过捆绑代表该参数的运行时代表来使其可恢复。
请注意,这种使用Sig
相当于Either (Example Int) (Example ())
-毕竟,sigma 类型是 sum - 但是当您对一个大(或可能是无限)集合求和时,这个版本的扩展性更好。
现在很容易将我们的表达式解析器构建成程序解析器。我们只需要重复应用表达式解析器,然后操作列表中的依赖对。
parseProgram :: Parser (Sig Ty Example)
parseProgram = fmap (foldr1 combine) $ parseExpr `sepBy1` (char '\n')
where combine (Sig _ val) (Sig ty acc) = Sig ty (val >> acc)
我向您展示的代码不是示例性的。它没有将解析和类型检查的关注点分开。在生产代码中,我将通过首先将数据解析为无类型语法树(一种不强制类型不变量的单独数据类型)来模块化此设计,然后通过类型检查将其转换为类型化版本。依赖对技术仍然需要为类型检查器的输出提供类型,但它不会在解析器中纠缠不清。
*如果不需要绑定,您是否考虑过使用免费的应用程序来表示您的数据?
†<code>Ex 和:*:
是我从Hasochism 论文中提取的可重复使用的机器