Allow hashing clauses

This commit is contained in:
Emi Simpson 2023-03-06 11:34:19 -05:00
parent 65a8c0042f
commit fe8d9b766c
Signed by: Emi
GPG key ID: A12F2C2FFDC3D847
2 changed files with 67 additions and 51 deletions

52
ir.py
View file

@ -2,7 +2,7 @@ from emis_funky_funktions import *
from dataclasses import dataclass from dataclasses import dataclass
from functools import reduce from functools import reduce
from typing import Sequence, TypeAlias from typing import Collection, FrozenSet, Sequence, TypeAlias
@dataclass(frozen=True) @dataclass(frozen=True)
class Subst: class Subst:
@ -62,7 +62,7 @@ class IRProp:
The identifier of this thing, including its location in the source The identifier of this thing, including its location in the source
""" """
arguments: 'Sequence[IRTerm]' = tuple() arguments: 'Tuple[IRTerm, ...]' = tuple()
def subst(self, subst: Subst) -> 'IRTerm': def subst(self, subst: Subst) -> 'IRTerm':
""" """
@ -78,7 +78,7 @@ class IRProp:
>>> original.subst(Subst('x1', IRProp('Alex'))) >>> original.subst(Subst('x1', IRProp('Alex')))
angry(Alex()) angry(Alex())
""" """
return IRProp(self.name, [arg.subst(subst) for arg in self.arguments]) return IRProp(self.name, tuple(arg.subst(subst) for arg in self.arguments))
def __str__(self) -> str: def __str__(self) -> str:
return repr(self) return repr(self)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -166,7 +166,17 @@ class IRNeg:
return var in self.inner return var in self.inner
IRTerm: TypeAlias = IRVar | IRProp | IRNeg IRTerm: TypeAlias = IRVar | IRProp | IRNeg
Clause: TypeAlias = Sequence[IRTerm] Clause: TypeAlias = FrozenSet[IRTerm]
Clause_: TypeAlias = Collection[IRTerm]
"""
A more general definition of `Clause` which uses a collection rather than a frozen set
Every `Clause` is a `Clause_`, but not vice versa. In other words, true `Clause` is a
subclass of `Clause_`.
Due to this generalization, `Clause_` does not necessarily benefit from hashability or
deduplication. It exists mostly to make inputing things easier, e.g. in doctests.
"""
sub_all: Callable[[Substitutions, IRTerm], IRTerm] = p(reduce, lambda t, s: t.subst(s)) #type:ignore sub_all: Callable[[Substitutions, IRTerm], IRTerm] = p(reduce, lambda t, s: t.subst(s)) #type:ignore
""" """
@ -181,19 +191,6 @@ Applies every substitution to the term in order
kismesis(Karkat(), Karkat()) kismesis(Karkat(), Karkat())
""" """
sub_all_clause: Callable[[Substitutions, Clause], Clause] = uncurry2(c(p_map, cur2(sub_all))) #type:ignore
"""
Perform a series of substitutions on every term in a list
Effectively calls `sub_all()` on every element of the list.
>>> sub_all_clause(
... [Subst('x1', IRVar('Dave')), Subst('x2', IRProp('Karkat'))],
... [IRProp('dating', [IRVar('x1'), IRVar('x2')]), IRVar('x1')],
... )
[dating(Dave(),Karkat()), Dave()]
"""
def unify(t1: IRTerm, t2: IRTerm) -> Result[Substitutions, UnificationError]: def unify(t1: IRTerm, t2: IRTerm) -> Result[Substitutions, UnificationError]:
""" """
Attempt to find a substitution that unifies two terms Attempt to find a substitution that unifies two terms
@ -228,28 +225,31 @@ def unify(t1: IRTerm, t2: IRTerm) -> Result[Substitutions, UnificationError]:
case (IRVar(v), t_other) | (t_other, IRVar(v)) if v not in t_other: #type: ignore case (IRVar(v), t_other) | (t_other, IRVar(v)) if v not in t_other: #type: ignore
return Ok((Subst(v, t_other),)) return Ok((Subst(v, t_other),))
case (IRProp(n1, a1), IRProp(n2, a2)) if n1 == n2 and len(a1) == len(a2): case (IRProp(n1, a1), IRProp(n2, a2)) if n1 == n2 and len(a1) == len(a2):
return unify_clauses(a1, a2) return unify_lists(a1, a2)
case (IRNeg(i1), IRNeg(i2)): case (IRNeg(i1), IRNeg(i2)):
return unify(i1, i2) return unify(i1, i2)
return Err(UnificationMismatch(t1, t2)) return Err(UnificationMismatch(t1, t2))
def unify_clauses(c1: Clause, c2: Clause) -> Result[Substitutions, UnificationError]: def unify_lists(c1: Sequence[IRTerm], c2: Sequence[IRTerm]) -> Result[Substitutions, UnificationError]:
""" """
Attempt to perform unification on two clauses or argument lists Attempt to perform unification on two term/argument lists
See `unify()` for the details of how this works. When working with clauses, the same See `unify()` for the details of how this works. When working with lists, the same
rules apply. The substitutions, when applied to every term of both clauses, will rules apply. The substitutions, when applied to every term of both lists, will
cause the clauses to become exactly the same. cause the lists to become exactly the same.
Lists which are not the same length cannot be unified, and will always fail. Lists which are not the same length cannot be unified, and will always fail.
>>> unify_clauses( Notice the difference between a list of `IRTerm`s and a `Clause`: Namely, that a
`Clause` is unordered, while a list of `IRTerm`s is ordered.
>>> unify_lists(
... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ], ... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ],
... [ IRProp('imaginary', [IRVar('x1')]), IRProp('friend', [IRVar('x2'), IRProp('Tavros')]) ] ... [ IRProp('imaginary', [IRVar('x1')]), IRProp('friend', [IRVar('x2'), IRProp('Tavros')]) ]
... ) ... )
Ok((Rufio()/x1, Rufio()/x2, Tavros()/x3)) Ok((Rufio()/x1, Rufio()/x2, Tavros()/x3))
>>> unify_clauses( >>> unify_lists(
... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ], ... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ],
... [ IRProp('imaginary', [IRVar('x1')]) ] ... [ IRProp('imaginary', [IRVar('x1')]) ]
... ) ... )
@ -260,7 +260,7 @@ def unify_clauses(c1: Clause, c2: Clause) -> Result[Substitutions, UnificationEr
return Ok(tuple()) return Ok(tuple())
case ([h1, *t1], [h2, *t2]): case ([h1, *t1], [h2, *t2]):
return unify(h1, h2) << (lambda subs: return unify(h1, h2) << (lambda subs:
unify_clauses((*map(p(sub_all,subs),t1),), (*map(p(sub_all,subs),t2),)) <= ( unify_lists((*map(p(sub_all,subs),t1),), (*map(p(sub_all,subs),t2),)) <= (
lambda final_subs: (*subs, *final_subs))) lambda final_subs: (*subs, *final_subs)))
case ([h, *t], []) | ([], [h, *t]): case ([h, *t], []) | ([], [h, *t]):
return Err(LengthMismatch(h)) return Err(LengthMismatch(h))

View file

@ -1,8 +1,19 @@
from emis_funky_funktions import * from emis_funky_funktions import *
from itertools import combinations, product from itertools import combinations, product
from typing import Collection, FrozenSet, TypeAlias
from ir import Clause, IRNeg, IRProp, IRTerm, IRVar, Substitutions, sub_all, unify, unify_clauses from ir import Clause, Clause_, IRNeg, IRProp, IRTerm, IRVar, Substitutions, sub_all, unify
KnowledgeBase: TypeAlias = FrozenSet[Clause]
KnowledgeBase_: TypeAlias = Collection[Clause_]
"""
A more general version of `KnowledgeBase`
`KnowledgeBase_` : `KnowledgeBase` :: `Clause_` : `Clause`
A superclass of `KnowledgeBase`
"""
def terms_cancel(t1: IRTerm, t2: IRTerm) -> Option[Substitutions]: def terms_cancel(t1: IRTerm, t2: IRTerm) -> Option[Substitutions]:
""" """
@ -29,53 +40,54 @@ def terms_cancel(t1: IRTerm, t2: IRTerm) -> Option[Substitutions]:
return hush(unify(x, IRNeg(a))) return hush(unify(x, IRNeg(a)))
return None return None
def merge_clauses(c1: Clause, c2: Clause) -> Sequence[Clause]: def merge_clauses(c1: Clause_, c2: Clause_) -> KnowledgeBase:
""" """
Produce a list of all possible clauses which resolution could derive from c1 and c2 Produce a list of all possible clauses which resolution could derive from c1 and c2
For each term in c1 that could cancel with a term in c2 using a substitution, a For each term in c1 that could cancel with a term in c2 using a substitution, a
possible clause is produced equal to the concatenation of c1 and c2 with the canceled possible clause is produced equal to the union of c1 and c2 with the canceled
terms removed and the substitution applied. terms removed and the substitution applied.
>>> merge_clauses( >>> merge_clauses(
... [ IRProp('day'), IRNeg(IRProp('night')) ], ... [ IRProp('day'), IRNeg(IRProp('night')) ],
... [ IRProp('night') ] ... [ IRProp('night') ]
... ) ... )
[[day()]] { { day() } }
>>> merge_clauses( >>> merge_clauses(
... [ IRNeg(IRProp('transgender', [IRVar('x1')])), IRProp('powerful', [IRVar('x1')]) ], ... [ IRNeg(IRProp('transgender', [IRVar('x1')])), IRProp('powerful', [IRVar('x1')]) ],
... [ IRNeg(IRProp('powerful', [IRVar('x2')])), IRProp('god', [IRVar('x2')]) ] ... [ IRNeg(IRProp('powerful', [IRVar('x2')])), IRProp('god', [IRVar('x2')]) ]
... ) ... )
[[¬transgender(*x1), god(*x1)]] { { god(*x1), ¬transgender(*x1) } }
>>> merge_clauses( >>> merge_clauses(
... [ IRNeg(IRProp('day')), IRProp('night') ], ... [ IRNeg(IRProp('day')), IRProp('night') ],
... [ IRVar('x2') ] ... [ IRVar('x2') ]
... ) ... )
[[night()], [¬day()]] { { night() }, { ¬day() } }
If two clauses cannot merge, an empty list is returned If two clauses cannot merge, an empty set is returned
>>> merge_clauses( >>> merge_clauses(
... [ IRProp('day') ], ... [ IRProp('day') ],
... [ IRProp('wet') ] ... [ IRProp('wet') ]
... ) ... )
[] { }
""" """
terms1, terms2 = list(c1), list(c2)
valid_substitutions = drop_none( valid_substitutions = drop_none(
map_opt(lambda subs: (subs, i1, i2), terms_cancel(t1, t2)) map_opt(lambda subs: (subs, i1, i2), terms_cancel(t1, t2))
for ((i1, t1), (i2, t2)) in product(enumerate(c1), enumerate(c2)) for ((i1, t1), (i2, t2)) in product(enumerate(terms1), enumerate(terms2))
) )
return [ return FSet(
[ FSet(
sub_all(subs, term) sub_all(subs, term)
for term in (*c1[:i1], *c1[i1 + 1:], *c2[:i2], *c2[i2 + 1:]) for term in (*terms1[:i1], *terms1[i1 + 1:], *terms2[:i2], *terms2[i2 + 1:])
] )
for (subs, i1, i2) in valid_substitutions for (subs, i1, i2) in valid_substitutions
] )
def derive(clauses: Sequence[Clause]) -> Sequence[Clause]: def derive(clauses: KnowledgeBase_) -> KnowledgeBase:
""" """
All possible clauses which derive in one step of resolution from a knowledge base All possible clauses which derive in one step of resolution from a knowledge base
@ -86,16 +98,20 @@ def derive(clauses: Sequence[Clause]) -> Sequence[Clause]:
... [IRNeg(IRProp('dog', [IRVar('x0')])), IRProp('animal', [IRVar('x0')])], ... [IRNeg(IRProp('dog', [IRVar('x0')])), IRProp('animal', [IRVar('x0')])],
... [IRNeg(IRProp('cat', [IRVar('x1')])), IRProp('animal', [IRVar('x1')])], ... [IRNeg(IRProp('cat', [IRVar('x1')])), IRProp('animal', [IRVar('x1')])],
... [IRProp('dog', [IRProp('Kim')])], ... [IRProp('dog', [IRProp('Kim')])],
... ]) ... ]) #doctest: +NORMALIZE_WHITESPACE
[[¬dog(Kim())], [¬cat(Kim())], [animal(Kim())]] {
{ animal(Kim()) },
{ ¬cat(Kim()) },
{ ¬dog(Kim()) }
}
""" """
return [ return FSet(
clause FSet(clause)
for (c1, c2) in combinations(clauses, 2) for (c1, c2) in combinations(clauses, 2)
for clause in merge_clauses(c1, c2) for clause in merge_clauses(c1, c2)
] )
def derive2(kb1: Sequence[Clause], kb2: Sequence[Clause]) -> Sequence[Clause]: def derive2(kb1: KnowledgeBase_, kb2: KnowledgeBase_) -> KnowledgeBase:
""" """
All clauses which derive in one step from the combination of two knowledge bases All clauses which derive in one step from the combination of two knowledge bases
@ -110,13 +126,13 @@ def derive2(kb1: Sequence[Clause], kb2: Sequence[Clause]) -> Sequence[Clause]:
... [IRNeg(IRProp('dog', [IRVar('x0')])), IRProp('animal', [IRVar('x0')])], ... [IRNeg(IRProp('dog', [IRVar('x0')])), IRProp('animal', [IRVar('x0')])],
... [IRProp('dog', [IRProp('Kim')])], ... [IRProp('dog', [IRProp('Kim')])],
... ]) ... ])
[[¬dog(Kim())]] { { ¬dog(Kim()) } }
""" """
return [ return FSet(
clause FSet(clause)
for (c1, c2) in product(kb1, kb2) for (c1, c2) in product(kb1, kb2)
for clause in merge_clauses(c1, c2) for clause in merge_clauses(c1, c2)
] )
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest