= Rubylog - Prolog interpreter for ruby
Rubylog is a Prolog-like DSL for Ruby. The language is inspired by {Jamis Buck}[http://weblog.jamisbuck.org/2006/10/28/prolog-in-ruby], and the implementation is based on {Yield Prolog}[http://yieldprolog.sourceforge.net/], with lots of sintactic and semantic candy.
See the {wiki}[https://github.com/cie/rubylog/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, 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
== 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 Rubylog terms, but this is planned (see hash.rb in the examples folder).
== 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 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
== Unification
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
== Guards
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.
== 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.
=== 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
=== Reporting bugs or requesting features
== Copyright
Copyright (c) 2013 Bernát Kalló. See LICENSE.txt for further details.