使用Haskell的LaTeX自然演绎样张

时间:2013-11-12 01:47:11

标签: haskell latex proof

如何通过Haskell为自然演绎证明树(like those shown here)创建LaTeX源,例如使用HaTeX?我想模仿bussproofs.styproof.sty之类的LaTeX .sty

1 个答案:

答案 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库会让事情复杂化。

我已经两次编写了证明树生成代码:

  1. 以独立的方式,通过编写类型检查器也是 返回证明树;

  2. 使用我的跟踪库,通过调用跟踪类型检查器然后 将跟踪处理后发布到证明树中。

  3. 由于您没有询问过呼叫跟踪库,因此您可能会更少 对基于呼叫追踪的版本感兴趣,但我认为它是 有趣的是比较两个版本。

    实施例

    让我们先从一些输出示例开始,看看这一切对我们有什么影响。 前三个例子是有动力的 an axiom system for implicational propositional calculus; 前两个也恰好对应S and K

    • 第一个公理,K,带有证据条款:

      K combinator, with proof terms

    • 第二个公理,S,带有证据条款,但有 上下文,而不是lambda bound:

      S combinator, via context, with proof terms

    • 第四个公理,模式,没有证明条款:

      Modus Ponens, without proof terms

    维基百科文章中的第三条公理(Peirce' s)是 非建设性的,所以我们不能在这里证明。

    对于其他类型的示例,此处Y combinator的类型检查失败:

    Y combinator, error, with proof terms

    箭头旨在引导您出现错误,该错误标有a bang(!)。

    代码

    现在我将描述生成这些示例的代码。代码是 来自this file 除非另有说明。我这里不包括每一行代码; 如果您想要使用GHC实际构建的东西,请查看该链接 7.6.3。

    大多数代码 - 语法,解析器和漂亮的打印机 - 都是 两个版本相同;只有类型的检查器和证明树 发电机不同。所有常见代码都在file just referenced

    STLC语法

    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
    

    版本1:自包含内联校样树生成

    此版本的完整代码是 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
    

    第2版:通过呼叫跟踪,将证明树生成为后期处理

    这里的类型检查器甚至不知道证明树生成,并且 添加呼叫跟踪只是一行。

    类型检查

    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 typeProofTree 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) 调试类型检查器的重要帮助。