Rubylog - Prolog interpreter for ruby

Rubylog is a Prolog-like DSL for Ruby. The language is inspired by Jamis Buck, and the implementation is based on Yield Prolog, with lots of sintactic and semantic candy.

See the wiki for online documentation.

Getting started

Installing

Currently, Rubylog only works with Ruby 1.9.2.

First, install the gem

$ gem install rubylog

or, if you use bundler, add this line to your Gemfile:

gem 'rubylog', '~>2.1'

The context

Secondly, you need a Rubylog context. The simplest you can do is to extend Rubylog::Context into the main object.

require 'rubylog'
extend Rubylog::Context

Another option is to use Kernel#rubylog:

require 'rubylog'

rubylog do
  # your code here
end

Data types

Rubylog is similar to Prolog, but there are quite a few differences.

Rubylog variables are (undefined) constant names:

A, B, ANYTHING

A variable can be bound or unbound, and it contains a Ruby object when bound. A variable whose name starts with ANY... (case-insensitive) is a don’t-care variable (like _ in Prolog).

Lists are just Ruby arrays:

[1, 2, 3]

They can have splats:

[1, 2, *T]

Which would be [1,2|T] in Prolog. However, in Rubylog, splats are not limited to the end:

[1, *X, 5]
[*A, *B]

Currently you cannot use hashes as terms.

Predicates

As in prolog, predicates are the building blocks of your program. However, the arguments are in a different order than they are in prolog:

'John'.likes('beer')

which would be likes('John','beer') in prolog. As you can see, the first argument comes first, then the functor, and then the further arguments.

In Rubylog, predicates must be declared with predicate_for:

predicate_for String, ".likes()"

You have to specify the class of the possible first arguments (String in this case), this is called the subject class. This could also be an array of classes. The string indicating the predicate syntax is ".likes()". The format is .asdf .asdf() .asdf(,) .asdf(,,) for predicates with 1,2,3 and 4 arguments. You can add descriptions in the indicator string e.g. "Person.likes(Drink)" means the same as ".likes()".

Declaring a predicate with arguments gives you three methods on the subject class:

predicate_for String, ".likes()"
'John'.likes('beer')    # returns a structure object representing this logical statement
'John'.likes!('beer')   # asserts this statement as a fact
'John'.likes?('beer')   # tells if this statement is true (in this case, returns true)

Nullary predicates

Nullary predicates are symbols, and they have to be declared with predicate:

predicate ":asdf"
:asdf

Asserting clauses

As in Prolog, there are two types of program clauses: facts and rules. You can assert facts with the bang syntax:

predicate_for String, ".likes()"
'John'.likes! 'milk'

This would be likes('John','beer'). in Prolog. Bang assertions return their first argument (which is 'John' in this case), so they can be chained:

'John'.likes!('beer').has!('beer')

You can assert rules with the if method:

predicate_for String, ".likes() .drinks() .has()"

X.drinks(Y).if X.has(Y).and X.likes(Y)

This would be drinks(X,Y) :- has(X,Y), likes(X,Y). in Prolog.

You can also use unless:

predicate_for String, ".good .bad"
A.good.unless A.bad

Unification

In Rubylog, unification works like in Prolog, but with the is functor.

A.is(B)

Using arrays, you can benefit the splats:

[1,2,3,4].is([A,B,*T])      # [1,2,3,4] = [A,B|T]     in prolog
[1,2,3,4].is([*H,*T])       # append(H, T, [1,2,3,4]) in prolog

The in predicate unifies the first argument with any member of the collection:

4.in([1,2,3,4])             # member(4,[1,2,3,4]) in prolog

Guards

You can use guards. These are constant expressions that restrict the unification of variables. There are several types of guards:

A[String].in(["asdf",5,nil]).each { p A }     # outputs "asdf"
A[/x/].in(["asdf","xyz"]).each { p A }        # outputs "xyz"
A[length: 3].in(["abc","abcd"]).each { p A }  # outputs "abc"
A[thats < 5].in([4,5,6]).each { p A }         # outputs 4
A[thats.length + 1 == 5].in(["abc","abcd"]).each { p A }    # outputs "abcd"

thats can receive any number of messages chained, and this will be applied to the value that would be bound to the variable. You can add various guards to a variable. thats_not is the negation of thats.

A[Integer, thats%2 == 0].even!

This is an experimental feature. Currently, you cannot use variables in guards, only constant values.

Moving between Ruby and Rubylog

Running a query

If you want to run a query, you have three different syntaxes:

true? ('John'.drinks 'beer')  # => true
('John'.drinks 'beer').true?  # => true
'John'.drinks? 'beer'         # => true

Finding solutions

Structure implements Enumerable, and yields the solutions. Within the enumeration block, you can access the values of your variables.

'John'.drinks! 'beer'
('John'.drinks X).each {p X}      # outputs 'beer'
('John'.drinks X).map{X}          # => ['beer']
('John'.drinks X).count           # => 1

You can also use solve, which is equivalent with each.

Procs as predicates

You can invoke Ruby codes in Rubylog rules with a proc:

'John'.likes(Y).if proc{ Y =~ /ale/ }

or in most cases you can use just a block:

'John'.likes(Y).if { Y =~ /ale/ }

The predicate succeeds if the block returns a true value.

Procs as functions

is and in can take a proc or block argument, which they execute and take its return value:

X.good.if X.is { 'BEER'.downcase }
X.good.if X.in { get_good_drinks() }

Variables in blocks

When you use blocks or procs as predicates or functions, or when you use enumeration methods with blocks, you can access values of variables by their name in the blocks (if they are bound).

'John'.likes(Y).if { Y =~ /ale/ }
'John'.likes(Y).if Y.is { Y =~ /ale/ }
'John'.likes(Y).each { p Y }

If your variable is unbound, you will get the variable object.

X.is(Y).each { p X.class }   # outputs 'Rubylog::Variable'

Rspec integration

Rubylog can integrate with RSpec. This enables you to use variables and predicates in specs.

require "rspec/rubylog"

describe "numbers", rubylog: true do
  specify do
    A.is(5).map{A}.should == [5]
  end
end

There is an assertion method called check that receives a predicate as an argument and raises an exception if it fails.

check 5.is(5)

Built-in predicates

Some built-in predicates and their Prolog equivalents:

Rubylog  Prolog
-------  ------ 
:true     true
:fail     fail
.and()    ,
.or()     ;
.false    \+
.is()     =
.is_not() =/=
.in()     member
:cut!     !

There are some new ones which do not exist in prolog.

  • .not_in() is the negation of .in().

Quantifiers

.all(), .any(), .one(), .none() are prediates that are analogous to their equivalents in Enumerable. They prove their first argument, and for each solution try to prove their second argument. If the second argument succeeds for all / any / exactly one / none of the solutions of the first argument, they succeed. Some examples:

predicate_for Integer, ".even"
X.even.if { X%2 == 0 } 
check X.in([2,4]).all(X.even)
check X.in([1,2,4]).any(X.even)
check X.in([1,2,3]).one(X.even)
check X.in([1,3]).none(X.even)

There is another similar predicate A.iff(B), that succeeds if for all solutions of A, B is true, and vice versa. For example,

check X.in([2,4]).iff(X.in(1..4).and X.even)

These predicates also have a prefix form, which can be used to create more naturally sounding program lines:

check all X.in([2,4]), X.even
check any X.in([1,2,4]), X.even
check one X.in([1,2,3]), X.even
check none X.in([1,3]), X.even
check iff X.in([2,4]), X.in(1..4).and(X.even)

There is another quantifier A.every(B) or every(A,B). This works similarly to .all(), but for each solution of A, creates a copy of B and chains them together with .and(). It can be useful for work with assumptions, see below. This is an experimental feature, and still contains bugs.

File system

You can make some queries on the file system:

check "README".filename_in "."
check "./README".file_in "."

X.dirname_in(".").each { puts X }

Reflection

You can make some metaprogramming with Rubylog

  predicate_for String, ".likes()"

  check "John".likes("Jane").structure(Pred, :likes, ["John", "Jane"])

  "John".likes(X).if X.likes("John")
  "Jane".likes!("John")
  check "John".likes("Jane").follows_from "Jane".likes("John")

  "John".likes!("milk")
  check "John".likes("milk").fact
  check "John".likes("beer").fact.false

end

Arithmetics

check 5.sum_of(2,3)
check 5.product_of(1,5)

These work as expected if you provide any two of the three paramters. For example,

10.sum_of(6,A).solve { p A }  # outputs 4

A.in(1..21).and(21.product_of(A,B)).each do
  p [A,B]
end # outputs pairs of divisors

Assumptions

An assumption is an assertion that gets erased at backtracking. There are several possibilites for assuming clauses.

A.assumed              # assumes A as a fact
A.assumed_if(B)        # assumes A.if(B)
A.assumed_unless(B)    # assumes A.unless(B)
A.rejected             # assumes A.if(:cut!.and :fail) to the beginning of the rule list
A.rejected_if(B)       # assumes A.if(B.and :cut!.and :fail) to the beginning of the rule list
A.rejected_unless(B)   # assumes A.if(B.false.and :cut!.and :fail) to the beginning of the rule list
A.revoked              # temporarily removes a rule which holds for A

These are experimental features.

Troubleshooting

You can turn on tracing with

Rubylog.trace

And turn off with

Rubylog.trace false

Or, you can trace a specific code block with

Rubylog.trace do
  ...
end

Contributing

To the language

  • If you have a suggestion for the language, submit an issue.

Reporting bugs or requesting features

Copyright © 2013 Bernát Kalló. See LICENSE.txt for further details.