如何通过Haskell为自然演绎证明树(like those shown here)创建LaTeX源,例如使用HaTeX?我想模仿bussproofs.sty或proof.sty之类的LaTeX .sty
。
答案 0 :(得分:8)
我以您的问题为借口改进和演示a Haskell call-tracing library I'm working on。在上下文中 跟踪,创建证明树的一种明显方法是跟踪类型 检查器然后将跟踪格式化为自然扣除证明。至 保持简单我的示例逻辑是simply-typed lambda calculus (STLC), 这对应于命题的蕴涵片段 intuitionistic logic
我正在使用proofs.sty
,但不是通过HaTeX
或任何其他Haskell
乳胶库。证明树的Latex非常简单并且使用了
Haskell Latex库会让事情复杂化。
我已经两次编写了证明树生成代码:
以独立的方式,通过编写类型检查器也是 返回证明树;
使用我的跟踪库,通过调用跟踪类型检查器然后 将跟踪处理后发布到证明树中。
由于您没有询问过呼叫跟踪库,因此您可能会更少 对基于呼叫追踪的版本感兴趣,但我认为它是 有趣的是比较两个版本。
让我们先从一些输出示例开始,看看这一切对我们有什么影响。
前三个例子是有动力的
an axiom system for implicational propositional
calculus;
前两个也恰好对应S
and
K
:
第一个公理,K
,带有证据条款:
第二个公理,S
,带有证据条款,但有
上下文,而不是lambda bound:
第四个公理,模式,没有证明条款:
维基百科文章中的第三条公理(Peirce' s)是 非建设性的,所以我们不能在这里证明。
对于其他类型的示例,此处Y combinator的类型检查失败:
箭头旨在引导您出现错误,该错误标有a
bang(!
)。
现在我将描述生成这些示例的代码。代码是 来自this file 除非另有说明。我这里不包括每一行代码; 如果您想要使用GHC实际构建的东西,请查看该链接 7.6.3。
大多数代码 - 语法,解析器和漂亮的打印机 - 都是 两个版本相同;只有类型的检查器和证明树 发电机不同。所有常见代码都在file just referenced。
ASCII中的STLC语法:
-- Terms
e ::= x | \x:T.e | e e
-- Types
T ::= A | T -> T
-- Contexts
C ::= . | C,x:T
相应的Haskell:
type TmVar = String
type TyVar = String
data Tm = Lam TmVar Ty Tm
| TmVar TmVar
| Tm :@: Tm
deriving Show
data Ty = TyVar TyVar
| Ty :->: Ty
deriving (Eq , Show)
type Ctx = [(TmVar,Ty)]
两个版本都实现了相同的抽象STLC类型检查器。在ASCII中:
(x:T) in C
---------- Axiom
C |- x : T
C,x:T1 |- e : T2
--------------------- -> Introduction
C |- \x:T1.e : T1->T2
C |- e : T1 -> T2 C |- e1 : T1
--------------------------------- -> Elimination
C |- e e1 : T2
此版本的完整代码是 here
证明树生成发生在类型检查器中,但实际上
证明树生成代码被分解为addProof
和。{
conclusion
。
-- The mode is 'True' if proof terms should be included.
data R = R { _ctx :: Ctx , _mode :: Bool }
type M a = Reader R a
extendCtx :: TmVar -> Ty -> M a -> M a
extendCtx x t = local extend where
extend r = r { _ctx = _ctx r ++ [(x,t)] }
-- These take the place of the inferred type when there is a type
-- error.
here , there :: String
here = "\\,!"
there = "\\,\\uparrow"
-- Return the inferred type---or error string if type inference
-- fails---and the latex proof-tree presentation of the inference.
--
-- This produces different output than 'infer' in the error case: here
-- all premises are always computed, whereas 'infer' stops at the
-- first failing premise.
inferProof :: Tm -> M (Either String Ty , String)
inferProof tm@(Lam x t e) = do
(et' , p) <- extendCtx x t . inferProof $ e
let et'' = (t :->:) <$> et'
addProof et'' [p] tm
inferProof tm@(TmVar x) = do
mt <- lookup x <$> asks _ctx
let et = maybe (Left here) Right mt
addProof et [] tm
inferProof tm@(e :@: e1) = do
(et , p) <- inferProof e
(et1 , p1) <- inferProof e1
case (et , et1) of
(Right t , Right t1) ->
case t of
t1' :->: t2 | t1' == t1 -> addProof (Right t2) [p , p1] tm
_ -> addProof (Left here) [p , p1] tm
_ -> addProof (Left there) [p , p1] tm
addProof
对应于其他版本中的proofTree
:
-- Given the inferred type, the proof-trees for all premise inferences
-- (subcalls), and the input term, annotate the inferred type with a
-- result proof tree.
addProof :: Either String Ty -> [String] -> Tm -> M (Either String Ty , String)
addProof et premises tm = do
R { _mode , _ctx } <- ask
let (judgment , rule) = conclusion _mode _ctx tm et
let tex = "\\infer[ " ++ rule ++ " ]{ " ++
judgment ++ " }{ " ++
intercalate " & " premises ++ " }"
return (et , tex)
conclusion
的代码对两个版本都是通用的:
conclusion :: Mode -> Ctx -> Tm -> Either String Ty -> (String , String)
conclusion mode ctx tm e = (judgment mode , rule tm)
where
rule (TmVar _) = "\\textsc{Axiom}"
rule (Lam {}) = "\\to \\text{I}"
rule (_ :@: _) = "\\to \\text{E}"
tyOrError = either id pp e
judgment True = pp ctx ++ " \\vdash " ++ pp tm ++ " : " ++ tyOrError
judgment False = ppCtxOnlyTypes ctx ++ " \\vdash " ++ tyOrError
这里的类型检查器甚至不知道证明树生成,并且 添加呼叫跟踪只是一行。
type Mode = Bool
type Stream = LogStream (ProofTree Mode)
type M a = ErrorT String (ReaderT Ctx (Writer Stream)) a
type InferTy = Tm -> M Ty
infer , infer' :: InferTy
infer = simpleLogger (Proxy::Proxy "infer") ask (return ()) infer'
infer' (TmVar x) = maybe err pure . lookup x =<< ask where
err = throwError $ "Variable " ++ x ++ " not in context!"
infer' (Lam x t e) = (t :->:) <$> (local (++ [(x,t)]) . infer $ e)
infer' (e :@: e1) = do
t <- infer e
t1 <- infer e1
case t of
t1' :->: t2 | t1' == t1 -> pure t2
_ -> throwError $ "Can't apply " ++ show t ++ " to " ++ show t1 ++ "!"
LogStream
type
和ProofTree
class
来自图书馆。 LogStream
是日志事件的类型
&#34;魔术&#34;
simpleLogger
日志。注意这一行
infer = simpleLogger (Proxy::Proxy "infer") ask (return ()) infer'
将infer
定义为infer'
的实际版本
类型检查器。这就是追踪monadic函数所需要做的一切!
我不知道simpleLogger
实际上如何在这里工作,但是
结果是每次调用infer
都会被记录,包括
上下文,参数和返回值,这些数据被分组
与所有记录的子句一起(这里只有infer
)。这将是
很容易手动为infer
编写这样的日志记录代码,但它很好
您不必使用图书馆。
要生成Latex证明树,我们实施ProofTree
发布
处理infer
的通话跟踪。该库提供proofTree
调用ProofTree
方法并组装证明的函数
树木;我们只需要指明打字的结论如何
判断将被格式化:
instance ProofTree Mode (Proxy (SimpleCall "infer" Ctx InferTy ())) where
callAndReturn mode t = conclusion mode ctx tm (Right ty)
where
(tm , ()) = _arg t
ty = _ret t
ctx = _before t
callAndError mode t = conclusion mode ctx tm (Left error)
where
(tm , ()) = _arg' t
how = _how t
ctx = _before' t
error = maybe "\\,!" (const "\\,\\uparrow") how
pp
次呼叫是用户定义的漂亮打印机;显然,
图书馆不知道如何打印您的数据类型。
因为调用可能是错误的 - 库会检测错误
- 我们必须说明如何格式化
成功和失败的电话。请参阅上面的Y-combinator示例
例如,一个失败的类型检查,对应于
这里有callAndError
个案例。
library's proofTree
function
非常简单:它使用当前构建proofs.sty
证明树
称为结论,子句作为前提:
proofTree :: mode -> Ex2T (LogTree (ProofTree mode)) -> String
proofTree mode (Ex2T t@(CallAndReturn {})) =
"\\infer[ " ++ rule ++ " ]{ " ++ conclusion ++ " }{ " ++ intercalate " & " premises ++ " }"
where
(conclusion , rule) = callAndReturn mode t
premises = map (proofTree mode) (_children t)
proofTree mode (Ex2T t@(CallAndError {})) =
"\\infer[ " ++ rule ++ " ]{ " ++ conclusion ++ " }{ " ++ intercalate " & " premises ++ " }"
where
(conclusion , rule) = callAndError mode t
premises = map (proofTree mode)
(_children' t ++ maybe [] (:[]) (_how t))
我在库中使用proofs.sty
因为它允许任意多次
虽然bussproofs.sty
适用于此STLC示例,但仍可使用前提
因为没有规则有超过五个前提(限制为
bussproofs
)。描述了两种Latex包
here
现在我们返回两个版本之间通用的代码。
定义上面使用的pp
的漂亮打印机相当长 -
它处理优先级和关联性,并以一种方式编写
如果有更多术语,例如,应该是可扩展的产品,都加入了
微积分 - 但大多是直截了当的。首先,我们建立了一个优先权
表和优先级和关联性感知的括号:
- Precedence: higher value means tighter binding.
type Prec = Double
between :: Prec -> Prec -> Prec
between x y = (x + y) / 2
lowest , highest , precLam , precApp , precArr :: Prec
highest = 1
lowest = 0
precLam = lowest
precApp = between precLam highest
precArr = lowest
-- Associativity: left, none, or right.
data Assoc = L | N | R deriving Eq
-- Wrap a pretty print when the context dictates.
wrap :: Pretty a => Assoc -> a -> a -> String
wrap side ctx x = if prec x `comp` prec ctx
then pp x
else parens . pp $ x
where
comp = if side == assoc x || assoc x == N
then (>=)
else (>)
parens s = "(" ++ s ++ ")"
然后我们定义各个漂亮的打印机:
class Pretty t where
pp :: t -> String
prec :: t -> Prec
prec _ = highest
assoc :: t -> Assoc
assoc _ = N
instance Pretty Ty where
pp (TyVar v) = v
pp t@(t1 :->: t2) = wrap L t t1 ++ " {\\to} " ++ wrap R t t2
prec (_ :->: _) = precArr
prec _ = highest
assoc (_ :->: _) = R
assoc _ = N
instance Pretty Tm where
pp (TmVar v) = v
pp (Lam x t e) = "\\lambda " ++ x ++ " {:} " ++ pp t ++ " . " ++ pp e
pp e@(e1 :@: e2) = wrap L e e1 ++ " " ++ wrap R e e2
prec (Lam {}) = precLam
prec (_ :@: _) = precApp
prec _ = highest
assoc (_ :@: _) = L
assoc _ = N
instance Pretty Ctx where
pp [] = "\\cdot"
pp ctx@(_:_) =
intercalate " , " [ x ++ " {:} " ++ pp t | (x,t) <- ctx ]
添加&#34;模式&#34;论证,使用相同的漂亮会很容易
打印机打印纯ASCII,这对其他人很有用
呼叫跟踪后处理器,例如(未完成)UnixTree
processor。
解析器对于示例并不重要,但当然我没有输入
示例输入术语直接作为Haskell Tm
s。
回想一下ASCII中的STLC语法:
-- Terms
e ::= x | \x:T.e | e e
-- Types
T ::= A | T -> T
-- Contexts
C ::= . | C,x:T
这个语法含糊不清:术语应用e e
和函数类型T -> T
没有给出的关联性
语法。但在STLC术语中,应用程序是关联的和功能性的
类型是正确的关联,因此相应的消除歧义
我们实际解析的语法是
-- Terms
e ::= e' | \x:T.e | e e'
e' ::= x | ( e )
-- Types
T ::= T' | T' -> T
T' ::= A | ( T )
-- Contexts
C ::= . | C,x:T
解析器可能太简单了 - 我没有使用languageDef
和
它的空白敏感 - 但它完成了工作:
type P a = Parsec String () a
parens :: P a -> P a
parens = Text.Parsec.between (char '(') (char ')')
tmVar , tyVar :: P String
tmVar = (:[]) <$> lower
tyVar = (:[]) <$> upper
tyAtom , arrs , ty :: P Ty
tyAtom = parens ty
<|> TyVar <$> tyVar
arrs = chainr1 tyAtom arrOp where
arrOp = string "->" *> pure (:->:)
ty = arrs
tmAtom , apps , lam , tm :: P Tm
tmAtom = parens tm
<|> TmVar <$> tmVar
apps = chainl1 tmAtom appOp where
appOp = pure (:@:)
lam = uncurry Lam <$> (char '\\' *> typing)
<*> (char '.' *> tm)
tm = apps <|> lam
typing :: P (TmVar , Ty)
typing = (,) <$> tmVar
<*> (char ':' *> ty)
ctx :: P Ctx
ctx = typing `sepBy` (char ',')
为了阐明输入术语的含义,以下是示例 Makefile:
# OUTFILE CONTEXT TERM
./tm2latex.sh S.ctx 'x:P->Q->R,y:P->Q,z:P' 'xz(yz)'
./tm2latex.sh S.lam '' '\x:P->Q->R.\y:P->Q.\z:P.xz(yz)'
./tm2latex.sh S.err '' '\x:P->Q->R.\y:P->Q.\z:P.xzyz'
./tm2latex.sh K.ctx 'x:P,y:Q' 'x'
./tm2latex.sh K.lam '' '\x:P.\y:Q.x'
./tm2latex.sh I.ctx 'x:P' 'x'
./tm2latex.sh I.lam '' '\x:P.x'
./tm2latex.sh MP.ctx 'x:P,y:P->Q' 'yx'
./tm2latex.sh MP.lam '' '\x:P.\y:P->Q.yx'
./tm2latex.sh ZERO '' '\s:A->A.\z:A.z'
./tm2latex.sh SUCC '' '\n:(A->A)->(A->A).\s:A->A.\z:A.s(nsz)'
./tm2latex.sh ADD '' '\m:(A->A)->(A->A).\n:(A->A)->(A->A).\s:A->A.\z:A.ms(nsz)'
./tm2latex.sh MULT '' '\m:(A->A)->(A->A).\n:(A->A)->(A->A).\s:A->A.\z:A.m(ns)z'
./tm2latex.sh Y.err '' '\f:A->A.(\x:A.f(xx))(\x:A.f(xx))'
./tm2latex.sh Y.ctx 'a:A->(A->A),y:(A->A)->A' '\f:A->A.(\x:A.f(axx))(y(\x:A.f(axx)))'
./tm2latex.sh
脚本只是在输出中调用pdflatex
上面描述的Haskell程序。 Haskell程序产生证据
树,然后将其包装在最小的Latex文档中:
unlines
[ "\\documentclass[10pt]{article}"
, "\\usepackage{proof}"
, "\\usepackage{amsmath}"
, "\\usepackage[landscape]{geometry}"
, "\\usepackage[cm]{fullpage}"
-- The most slender font I could find:
-- http://www.tug.dk/FontCatalogue/iwonalc/
, "\\usepackage[light,condensed,math]{iwona}"
, "\\usepackage[T1]{fontenc}"
, "\\begin{document}"
, "\\tiny"
, "\\[" ++ tex ++ "\\]"
, "\\end{document}"
]
正如您所看到的,大多数Latex都致力于制作证明树 尽可能小;我打算写一个ASCII校样树帖 处理器,在实例中可能在实践中更有用 大。
与往常一样,编写解析器,类型检查器和编写器需要一些代码 漂亮的打印机最重要的是,添加证明树生成是 这两个版本都非常简单。这是一个有趣的玩具示例,但我 期望在&#34;真实&#34;的背景下做类似的事情。 基于统一的类型检查器,用于依赖类型的语言;那里 我希望提供呼叫跟踪和证明树生成(ASCII) 调试类型检查器的重要帮助。