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.
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'
Secondly, you need a Rubylog context, which is a portion of the code where Rubylog can be used (e.g. variables are interpreted correctly).
The simplest you can do is to create a file with Rubylog code and run it with rubylog
. This is recommended for prototypes, and for applications fully written in Rubylog.
$ rubylog myfile.rb
The second option is to use a module in one of normal Ruby files. This is the recommended way for bigger applications where Rubylog code is only a part of the project.
require 'rubylog' module MyRubylogStuff extend Rubylog::Context # Rubylog code here end
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 Rubylog terms, but this is planned (see hash.rb in the examples folder).
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 are symbols, and they have to be declared with predicate
:
predicate ":asdf" :asdf
As in Prolog, there are two types of program clauses: facts and rules. You can assert facts with the bang version of the predicate method:
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
In Rubylog, unification works like in Prolog, but with the is
functor.
A.is(B)
Using arrays, you can benefit from 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
You can use guards. These are constant expressions that restrict the unification of variables. There are several types of guards: classes, regexps, hashes and thats
expressions.
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
is a method that returns a special object, which 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.
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
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
.
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.
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() }
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'
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)
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()
.
.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.
You can make some queries on the file system:
check "README".filename_in "." check "./README".file_in "." X.dirname_in(".").each { puts X }
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
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
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.
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
-
If you have a suggestion for the language, submit an issue.
-
Create an issue on the issue tracker.
Copyright © 2013 Bernát Kalló. See LICENSE.txt for further details.