cie / rubylog

A Prolog DSL and interpreter for Ruby
MIT License
31 stars 1 forks source link

= 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.