Current section
5 Versions
Jump to
Current section
5 Versions
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…