Getting started with Logos
What is Logos?
Logos: The Greek (λόγοϚ) root of the word logic.
Logos
is a relatively small Elixir-hosted logic (aka relational) programming language
adapted from miniKanren and µKanren. An attempt was made to create an interface that looks
like idiomatic Elixir and feels approachable to someone who has no experience with
relational programming beyond Elixir pattern matching. Many of the core concepts and
implementation details were adopted directly from the book, The Reasoned Schemer, and
the µKanren paper (see references below). However, many function names have been updated,
either to be more descriptive and memorable, or due to Elixir conventions. Also, to enhance
the user experience in Elixir, some of the relational primitives in Logos
differ in
structure from their counterparts in miniKanren.
This library grew out of a general interest in logic programming and its potential
applications to software-based products. Because usage and extension of Logos
are
key design considerations, the library is broken into modules that reflect the different
implementation concerns, including unification, a purely functional core reminiscent of
µKanren, and a collection of macros that make up the main user interface. It is hoped
that Logos
can be applied, adapted, and grown with relative ease. Documentation for each
of the modules, functions, and macros is extensive, and includes many usage examples, as
well as notes on specific design choices and possible areas for future improvement
(WIP note: This is not yet true, but it will be!).
Questions (WIP)
Why now?
Why Elixir?
What’s different?
What’s new?
What are the applications?
A sample of references that influenced this work
µKanren: A Minimal Functional Core for Relational Programming
Relational Programming in miniKanren: Techniques, Applications, and Implementations
Quick start
To get started with right away, first install the Logos package as a mix
dependency.
For a Livebook, use Mix.install
:
Mix.install([{:logos, git: "https://github.com/epfahl/logos.git"}])
Now type use Logos
at the head of a module or in a Livebook session.
use Logos
In the calling module, this expands to
import Logos.Core, only: [all: 1, any: 1, equal: 2, failure: 0, success: 0]
import Logos.Interface, only: [ask: 2, choice: 1, defrule: 2, fork: 1, with_vars: 2]
import Logos.Rule
This brings in a set of common logical rules as well as the macros ask
, with_vars
,
and fork
. Now that the basic functionality is accessible, let’s see Logos
in action
with several small examples.
Ask a question
To get acclimated to the usage of Logos
, start with a simple algebra question: What is the value of the variable x
when x
is equal to 1? This isn’t a trick; the answer, x = 1
, is in the phrasing of the question. In Logos
, this question is expressed as
ask [x] do
equal(x, 1)
end
In words, this code could be read as, “Ask for the value of x
when x
equals 1.”
The macro ask
takes a list of variables as its argument, a logical goal as a body,
and returns an Elixir Stream
. Each item in the stream is a list of values of the
variables passed to ask
(x
in the above example). To get a list of results, pipe
the stream into Enum.to_list
:
ask [x] do
equal(x, 1)
end
|> Enum.to_list()
When Logos
evaluates a query, an attempt is made to satisfy the goal. However, it may
be that the goal expresses a false relationship, as shown here:
ask [x] do
equal(1, 2)
end
|> Enum.to_list()
The result is an empty stream, meaning that there is no state for which the goal is true.
Several specialized words are used above and in what follows, and it’s important to have
some early appreciation of their meanings. A rule expresses a logical relationship among
terms. A term can be a variable or a constant (number, string, or atom), or a list of
variables and constants. The equivalence rule is expressed as equal(x, y)
, where x
and y
are any two terms. When x
is a variable and y
is set to the constant 1,
equal(x, 1)
is a logical goal asserting that the variable x
is equal to 1. When a
goal is wrapped with ask
, that goal is posed as a query for we’d like answer.
To show that equal
works for list terms, find x
and y
in the following query:
ask [x, y] do
equal([x, 2, 3], [1, 2, y])
end
|> Enum.to_list()
Because the algorithm that performs this matching is recursive, list terms with any depth can be used:
ask [x, y] do
equal(
[1, [2, [x, y]]],
[1, [2, [3, [4, 5]]]]
)
end
|> Enum.to_list()
Here the value of x
is 3, and y
is the list [4, 5]
.
A stream of answers
Just like in algebra, a query in Logos
can have more than one answer–possibly an
infinite number. This is why ask
returns a stream. An open-ended question shows the
need for a stream: What are the lists for which the value :item
is a member? The
builtin Logos
rule member(item, list)
can be used to ask this question:
ask [l] do
member(:item, l)
end
|> Enum.take(10)
Intuitively, it’s apparent that there is an infinite collection of lists that satisfy
this query–lists of all possible lengths where :item
is in any available position.
The result above shows 10 such lists from the infinite stream. Other values in the lists
of the form :_
are variable slots that are not constrained by the query and
can hold any value.
Blending and sugaring rules
More interesting programs can be built by combining simple rules. The two mechanisms used
to combine rules are disjunction (“or” logic) and conjunction (“and” logic). In
Logos
, the two corresponding rules are any
and all
, both of which take a list of
zero or more goals as arguments.
This question illustrates the use of conjunctive logic: If x
equals y
and y
equals
1, what is the value of x
? The following query asks this question in Logos
and
introduces with_vars
, a macro that injects new variables into a goal:
ask [x] do
with_vars [y] do
all([
equal(x, y),
equal(y, 1)
])
end
end
|> Enum.to_list()
Logos
allows implicit conjunctions in ask
, where a list of goals [g1, g2, ...]
is interpreted as all([g1, g2, ...])
, so that the above query can be simplified to
ask [x] do
with_vars [y] do
[
equal(x, y),
equal(y, 1)
]
end
end
|> Enum.to_list()
Another simple question and corresponding Logos
query demonstrate the use of any
:
If x
can equal “a” or “b” or “c”, what are the values of x
?
ask [x] do
any([
equal(x, "a"),
equal(x, "b"),
equal(x, "c")
])
end
|> Enum.to_list()
As expected, the result has 3 possible solution states. Notice that any([...])
in the
above query could be replaced with member(x, ["a", "b", "c"])
.
The primitive rules any
and all
can be combined to exhibit more interesting behaviors:
ask [x, y] do
any([
all([equal(x, "a"), equal(y, 1)]),
all([equal(x, "b"), equal(y, 2)]),
all([equal(x, "c"), equal(y, 3)])
])
end
|> Enum.to_list()
Queries like this–a disjunction of conjunctions–are common in logic programming. Logos
offers the macro fork
to reduce the syntactic load of these expressions:
ask [x, y] do
fork do
[equal(x, "a"), equal(y, 1)]
[equal(x, "b"), equal(y, 2)]
[equal(x, "c"), equal(y, 3)]
end
end
|> Enum.to_list()
Logos
goes one step further with syntactic sugar by making the fork
syntax available
in both ask
and with_vars
. The above query can be written more simply as
ask [x, y] do
[equal(x, "a"), equal(y, 1)]
[equal(x, "b"), equal(y, 2)]
[equal(x, "c"), equal(y, 3)]
end
|> Enum.to_list()
When a conjunction clause has only one item, the list may be omitted:
ask [x] do
equal(x, 1)
with_vars [y] do
[equal(x, y), equal(y, 2)]
end
end
|> Enum.to_list()
While there are extensions to the core language, such as arithmetic operators and “impure”
rules, all “pure” logical goals can be build from equal
and with_vars
when implicit
conjunction and disjunction are used.