Packages

Guesswork is a logic programming library for Elixir. It is heavily inspired by Prolog, but attempts to use idiomatic Elixir when expressing problems and their solutions.

Current section

5 Versions

Jump to

Compare versions

43 files changed
+2438 additions
-773 deletions
  @@ -1,5 +1,9 @@
1 1 # Used by "mix format"
2 2 [
3 3 plugins: [DoctestFormatter],
4 - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
5 + locals_without_parens: [deffact: 1, deffalsehood: 1],
6 + export: [
7 + locals_without_parens: [deffact: 1, deffalsehood: 1]
8 + ]
5 9 ]
  @@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 7
8 + ## [0.7.0] - 2024-12-29
9 +
10 + ### Added
11 +
12 + - Added code coverage tracking.
13 + - Added `Guesswork.Arithmetic.Polynomial`, which allows the user to input a simple
14 + equation, and then translates it into logical constraints
15 + (`Guesswork.Constraint.Arithmetic`).
16 + - A new protocol, `Guesswork.Constraint` has been added that allows multi
17 + directional relationships to be defined.
18 + These constraints are stored using a trie in answer sets and now replace
19 + computations in bindings as well as test computations and stop conditions.
20 + - To facilitate this `Guesswork.Ast.Is` now supports a `halt_on_error` option
21 + that turns it into a stop condition.
22 + - These constraints now support relationships beyond simple equality, including
23 + less than, greater than, less than or equal or, and greater than or equal to.
24 + - Constraints can be displayed along with answer sets.
25 + - Added a new server, `Guesswork.Constraint.Arithmetic.InternalVariable` that can
26 + be run by the user and enables short, readable, and unique variables.
27 +
28 + ### Fixed
29 +
30 + - `Guesswork.KnowledgeBase.Collection.deffact/2` and
31 + `Guesswork.KnowledgeBase.Collection.deffalsehood/2` now accept variables and
32 + follow normal matching rules.
33 + This replaces `Guesswork.Ast.Wildcard` and allows values to be propagated across
34 + the statement.
35 + - The definitions in `Guesswork.KnowledgeBase.Collection` now all use
36 + standard function syntax.
37 + This changes the type of `Guesswork.Ast.Fact` some, so now the `:relationship`
38 + is an atom.
39 + - Constraints are now removed as their variables fall out of scope.
40 + - Resolving a rule now returns a closure that takes an env and returns an
41 + answer stream instead of just returning a stream (changing the return type of
42 + `Guesswork.Ast.Statement.resolve/2`).
43 + This allows values to be substituted incrementally instead of just unioned.
44 + As a side-effect of this change the negated value used for constraints (and
45 + only constraints) is reset when resolving a rule.
46 + - As a result of the changes to fact resolution, precomputation is now done at
47 + the fact level with an ets cache.
48 + This can create problems with recursive rules so the default `precompute_count`
49 + is now 0.
50 +
51 + ### Removed
52 +
53 + - `Guesswork.Ast.Stop` has been removed in favor of constraints.
54 +
8 55 ## [0.6.0] - 2024-10-10
9 56
10 57 ### Added
  @@ -8,7 +8,7 @@ expressing problems and their solutions.
8 8
9 9 This project is tested on the following elixir/OTP versions:
10 10 - erlang 26.2.5
11 - - elixir 1.16.3
11 + - elixir 1.17.3
12 12
13 13 ## Examples
  @@ -2,7 +2,7 @@
2 2
3 3 ```elixir
4 4 Mix.install([
5 - {:guesswork, "~> 0.6"}
5 + {:guesswork, "~> 0.7"},
6 6 {:kino, "~> 0.13"}
7 7 ],
8 8 consolidate_protocols: false
  @@ -25,20 +25,20 @@ Start by adding rules and concrete facts to the knowledge base, in this case par
25 25 defmodule KB do
26 26 use Guesswork.KnowledgeBase.Collection
27 27
28 - deffact("parent", [:bob, :joe])
29 - deffact("parent", [:bob, :jean])
30 - deffact("parent", [:mary, :joe])
31 - deffact("parent", [:bobby, :jean])
28 + deffact parent(:bob, :joe)
29 + deffact parent(:bob, :jean)
30 + deffact parent(:mary, :joe)
31 + deffact parent(:bobby, :jean)
32 32
33 - deffact("parent", [:christy, :bob])
34 - deffact("parent", [:lance, :bob])
33 + deffact parent(:christy, :bob)
34 + deffact parent(:lance, :bob)
35 35
36 - deffact("parent", [:billy, :christy])
37 - deffact("parent", [:meg, :lanceb])
36 + deffact parent(:billy, :christy)
37 + deffact parent(:meg, :lanceb)
38 38
39 - defrule("grandparent", [gp, gc]) do
40 - Fact.new("parent", [gp, p])
41 - Fact.new("parent", [p, gc])
39 + defrule grandparent(gp, gc) do
40 + Fact.new(:parent, [gp, p])
41 + Fact.new(:parent, [p, gc])
42 42 end
43 43 end
44 44 ```
  @@ -51,7 +51,7 @@ concrete facts stored in the knowledge base; in this case what are
51 51 bob's children.
52 52
53 53 ```elixir
54 - Guesswork.query(term(Fact.new("parent", [:bob, child])), 10, knowledge_base: KB)
54 + Guesswork.query(term(Fact.new(:parent, [:bob, child])), 10, knowledge_base: KB)
55 55 ```
56 56
57 57 You can also ask more complex questions, like what are all the grandparents
  @@ -61,5 +61,5 @@ part of the final answer set.
61 61 This is because `Guesswork` respects lexical scoping of variables.
62 62
63 63 ```elixir
64 - Guesswork.query(term(Fact.new("grandparent", [gp, :joe])), 100, knowledge_base: KB)
64 + Guesswork.query(term(Fact.new(:grandparent, [gp, :joe])), 100, knowledge_base: KB)
65 65 ```
  @@ -2,7 +2,7 @@
2 2
3 3 ```elixir
4 4 Mix.install([
5 - {:guesswork, "~> 0.6"}
5 + {:guesswork, "~> 0.7"}
6 6 ])
7 7 ```
8 8
  @@ -14,7 +14,6 @@ import Guesswork.Ast
14 14 alias Guesswork.Ast.And
15 15 alias Guesswork.Ast.Fact
16 16 alias Guesswork.Ast.Is
17 - alias Guesswork.Ast.Stop
18 17 ```
19 18
20 19 Looking for a factorial is a little different from some of the other queries
  @@ -29,13 +28,14 @@ the recursive step (where `Fact` invoces the next step).
29 28 defmodule FactorialKB do
30 29 use Guesswork.KnowledgeBase.Collection
31 30
32 - deffact("n_factorial", [0, 1])
33 - defrule("n_factorial", [n, f]) do
34 - Stop.new([n, n1], &Kernel.</2)
35 - Stop.new([f, f1], &Kernel.</2)
31 + deffact n_factorial(0, 1)
32 + defrule n_factorial(n, f) do
33 + Is.new(true, [n, 0], &Kernel.>/2, halt_on_error: true)
34 + Is.new(true, [f, f1], &Kernel.>=/2, halt_on_error: true)
36 35 Is.new(n, [n1, 1], &Kernel.+/2)
36 + Is.new(n1, [n, 1], &Kernel.-/2)
37 37 Is.new(f, [n, f1], &Kernel.*/2)
38 - Fact.new("n_factorial", [n1, f1])
38 + Fact.new(:n_factorial, [n1, f1])
39 39 end
40 40 end
41 41 ```
  @@ -44,27 +44,27 @@ This can be used simillarly to other queries, like the pythagorean triple query,
44 44 we can pull the first `n` valid answer sets.
45 45
46 46 ```elixir
47 - Guesswork.query(term(Fact.new("n_factorial", [n, f])), 10, knowledge_base: FactorialKB)
47 + Guesswork.query(term(Fact.new(:n_factorial, [n, f])), 10, knowledge_base: FactorialKB)
48 48 ```
49 49
50 50 But because the query is defined in terms of `n` and `f` we can query what `f` is
51 51 when `n` is `5`.
52 52
53 53 ```elixir
54 - Guesswork.query(term(Fact.new("n_factorial", [5, f])), 10, knowledge_base: FactorialKB)
54 + Guesswork.query(term(Fact.new(:n_factorial, [5, f])), 10, knowledge_base: FactorialKB)
55 55 ```
56 56
57 57 And the system supports binding `f` and searching for `n`.
58 58
59 59 ```elixir
60 - Guesswork.query(term(Fact.new("n_factorial", [n, 120])), 10, knowledge_base: FactorialKB)
60 + Guesswork.query(term(Fact.new(:n_factorial, [n, 120])), 10, knowledge_base: FactorialKB)
61 61 ```
62 62
63 63 Even when there are multiple valid values for `n`, the query will return them all
64 64 (provided you ask for them).
65 65
66 66 ```elixir
67 - Guesswork.query(term(Fact.new("n_factorial", [n, 1])), 10, knowledge_base: FactorialKB)
67 + Guesswork.query(term(Fact.new(:n_factorial, [n, 1])), 10, knowledge_base: FactorialKB)
68 68 ```
69 69
70 70 ## Stop Conditions
  @@ -72,19 +72,19 @@ Guesswork.query(term(Fact.new("n_factorial", [n, 1])), 10, knowledge_base: Facto
72 72 If you look above, you'll notice that even though we ask for `10` results
73 73 from `Guesswork.query/3`, we only get the valid values, and then the query
74 74 terminates.
75 - This is done using stop conditions, via the `Stop` statement.
75 + This is done using stop conditions, via the `:halt_on_error` option of `Is`.
76 76
77 77 Stop conditions can also allow us to use the query to search for potentially
78 78 invalid values.
79 79
80 80 ```elixir
81 - Guesswork.query(term(Fact.new("n_factorial", [n, 121])), 10, knowledge_base: FactorialKB)
81 + Guesswork.query(term(Fact.new(:n_factorial, [n, 121])), 10, knowledge_base: FactorialKB)
82 82 ```
83 83
84 84 Or test if a relationship is valid between some `n` and `f`.
85 85
86 86 ```elixir
87 - Guesswork.query(term(Fact.new("n_factorial", [4, 120])), 10, knowledge_base: FactorialKB)
87 + Guesswork.query(term(Fact.new(:n_factorial, [4, 120])), 10, knowledge_base: FactorialKB)
88 88 ```
89 89
90 90 Stop conditions will terminate the entire stream after they are triggered
Loading more files…