Demo of the BeHOLd package
View SourceMix.install([
{:kino, "~> 0.17.0"},
{:behold, "~> 1.1.2"},
{:hol, "~> 1.0.2"}
])Setup
Definitions and patterns of e.g., HOL connectives, are defined in the modules BeHOLd.ClassicalHOL.Definitions and BeHOLd.ClassicalHOL.Patterns as macros. To use them in Livebook, we hence need the use keyword which handles the imports and ensures the macros are usable. Aliasing can be handled when passing the as option.
# Definitions and patterns are defined as macros.
# For usage in a livebook, we hence need the use keyword.
use BeHOLd.ClassicalHOL.Definitions, as: Definitions
use BeHOLd.ClassicalHOL.Patterns, as: Patterns
alias BeHOLd.ClassicalHOL.Equality
alias BeHOLd.Data.Context
alias BeHOLd.{Parser, TPTP}As BeHOLd is an extention of the library HOL, some functionality, e.g., the definition of additional types, require some of its modules. To use macros for pattern matching in Livebook, we need to require the desired module. The records for types and terms are internally defined as macros for tuples.
require HOL.Data
alias HOL.Data
alias PrettyPrintPer default, HOL prints debug logs to the console when calling functions from its API, mainly on levels :info and :notice. This behavior can be disabled by setting the level to :error or disable logging entirely.
Logger.put_application_level(:hol, :error)
form =
Kino.Control.form(
[
name: Kino.Input.checkbox("Print debug logs")
],
report_changes: true
)
Kino.listen(form, fn event ->
if event.data.name == true do
Logger.put_application_level(:hol, :info)
else
Logger.put_application_level(:hol, :error)
end
end)
formTypes
Types are instances of the data structure HOL.Data.type() and represents types as records which in turn internally use tuples. The type $\iota$ is hence representable (and can be pattern matched) as:
Definitions.type_i()match?(HOL.Data.type(goal: :i, args: []), Definitions.type_i())More complex types are stored in their curried, flattened representation, where the field :args contains the list of argument types. E.g., the type $(\iota\to o)\to(\iota\to o)\to(\iota\to o)$ can be curried as $(\iota\to o)\to(\iota\to o)\to\iota\to o$, where $o$ is the goal type and $(\iota\to o)$, $(\iota\to o)$ and $\iota$ are the argument types.
Definitions.type_io_io_io()match?(
HOL.Data.type(goal: :o, args: [
Definitions.type_io(),
Definitions.type_io(),
Definitions.type_i()
]),
Definitions.type_io_io_io()
)Definitions and Patterns for HOL Symbols
The module BeHOLd.ClassicalHOL.Definitions also contains the usual connectives of classical HOL as signature symbols and defines other common connectives like XOR in terms of these symbols. The signature spans the following connectives:
$$ \top_o \quad \bot_o \quad \neg_{o\to o} \quad \lor_{o\to o\to o} \quad \land_{o\to o\to o} \quad \supset_{o\to o\to o} \quad \equiv_{o\to o\to o} \quad \Pi_{(\alpha\to o)\to o} \quad \Sigma_{(\alpha\to o)\to o} \quad =_{\alpha\to\alpha\to o} $$
Definitions.signature_symbols()In combination with the functions from HOL.Data and HOL.Terms, these can be used to construct logical terms. The HOL library also includes a PrettyPrint module which can convert various data structures to a readable string representation, with or without type annotation.
x = HOL.Terms.mk_free_var_term("X", Definitions.type_o())
c = HOL.Terms.mk_const_term("c", Definitions.type_o())
x_and_c = Definitions.and_term()
|> HOL.Terms.mk_appl_term(x)
|> HOL.Terms.mk_appl_term(c)PrettyPrint.pp_term x_and_cPrettyPrint.pp_term x_and_c, falseNote that the connectives are implemented both as HOL.Data.declaration() and HOL.Data.hol_term() for different use cases, e.g., term construction or pattern matching.
match?(Definitions.and_const(), HOL.Data.get_head(x_and_c))$\lambda$-abstractions are also supported by specifying the variable (as HOL.Data.declaration()) to capture.
x_var = HOL.Data.mk_free_var("X", Definitions.type_o())
HOL.Terms.mk_abstr_term(x_and_c, x_var)
|> PrettyPrint.pp_term(false)The module BeHOLd.ClassicalHOL.Patterns provides various convenient macros to pattern-match on HOL terms.
match?(Patterns.conjunction(_, _), x_and_c)For other common connectives, shorthand constructors are also available which define them in terms of the symbols from the signature.
Definitions.xor_term()
|> PrettyPrint.pp_termQuantors and equality symbols require types for term construction, not necessarily for pattern matching. Note that for quantors, a set type (i.e., a type with goal $o$) must be specified.
a = HOL.Terms.mk_const_term("a", Definitions.type_i())
b = HOL.Terms.mk_const_term("b", Definitions.type_i())
a_eq_b =
Definitions.equals_term(Definitions.type_i())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
match?(Patterns.equality(^a, ^b), a_eq_b)TPTP Output
Terms and types can also be represented as TPTP strings via the BeHOLd.TPTP module. The BeHOLd.TPTP.term_to_tptp/1 function fully $\eta$-reduces terms and tries to reconstruct the syntactic sugar of TPTP, e.g., the XOR operator.
p = HOL.Terms.mk_free_var_term("P", Definitions.type_o())
q = HOL.Terms.mk_const_term("q", Definitions.type_o())
Definitions.xor_term()
|> HOL.Terms.mk_appl_term(p)
|> HOL.Terms.mk_appl_term(q)
|> TPTP.term_to_tptpParser
The module BeHOLd.Parser contains parsing utility for types and terms in TPTP's TH0 syntax. The parser implements full type inference, where terms of unknown will be mapped to type $o$. Optionally, a context can be specified for types which can't be inferred by the term structure. The parser implements the standard TH0 precedence rules.
Parser.parse "~A & B"The usage of quantors requires extra caution as their scope is influenced by parentheses.
"![X : $o]: $false => (X => $true)"
|> Parser.parse
|> HOL.Data.get_head"![X : $o]: (f @ X) | (g @ X)"
|> Parser.parse
|> HOL.Data.get_headA type environment (context) can be specified to clear up non-inferrable types. The type inference engine handles the rest.
"X = a" |> Parser.parse() |> PrettyPrint.pp_term(false)ctx = Context.new()
|> Context.put_var("X", Definitions.type_i())
"X = a" |> Parser.parse(ctx) |> PrettyPrint.pp_term(false)Equality
From version 1.1.2, BeHOLd supports constructors for different notions of equality. These include Leibniz equality $\mathcal{L}^\tau$, Andrews equality $\mathcal{A}^\tau$ and extensional equality $\mathcal{E}^{\alpha\to\beta}$.
$\mathcal{L}^\tau$ states that two terms of type $\tau$ are equal because they share the same properties. It can be formulated using different connectives such as implication, equivalence or the converse of the implication. Default is equivalence.
$\mathcal{A}^\tau$ formulates equality stating that all reflexive binary relations on type $\tau$ hold for the two terms.
$\mathcal{E}^{\alpha\to\beta}$ is only defined on function types and states that two terms are equal because they share the same extensions.
a = HOL.Terms.mk_const_term("a", Definitions.type_ii())
b = HOL.Terms.mk_const_term("b", Definitions.type_ii())
ctx = Context.new()
|> Context.put_const("a", Definitions.type_ii())
|> Context.put_const("b", Definitions.type_ii())(
Equality.leibniz_equality(Definitions.type_ii())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
) == Parser.parse("![P:($i>$i)>$o]: P @ a <=> P @ b")(
Equality.leibniz_equality(Definitions.type_ii(), Definitions.implies_term())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
) == Parser.parse("![P:($i>$i)>$o]: P @ a => P @ b")(
Equality.leibniz_equality(Definitions.type_ii(), Definitions.implied_by_term())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
) == Parser.parse("![P:($i>$i)>$o]: P @ a <= P @ b")(
Equality.andrews_equality(Definitions.type_ii())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
) == Parser.parse("![Q:($i>$i)>($i>$i)>$o]: ((![X:$i>$i]: Q @ X @ X) => Q @ a @ b)")(
Equality.extensional_equality(Definitions.type_ii())
|> HOL.Terms.mk_appl_term(a)
|> HOL.Terms.mk_appl_term(b)
) == Parser.parse("![X:$i]: a @ X = b @ X", ctx)