No namespace pollution - wizardofosmium/porolog GitHub Wiki

Creating a Logic Engine without Adding Methods

This example shows:

  • how to define predicates without creating methods, and
  • how to create separate independent engines per instance.

The commands

  builtin   :member
  predicate :prime

provide handy shortcuts to the predicates by creating methods to access them. The default adds these methods to Object so they are available everywhere. In small programs, this can be quite useful, but in large programs, this can be quite a problem; called namespace pollution.

The commands

  builtin   :member,  class_base: Widget
  predicate :prime,   class_base: Widget

also provide handy shortcuts to the predicates by creating methods to access them; however, using class_base: Widget, the methods are added to Widget as class methods and as instance methods, so they are available everywhere in the class Widget. This scoping of methods is probably sufficient in most cases.

If you need to completely avoid creating methods, you can simply create the predicates and store them in instance variables by calling Porolog::Predicate.new directly.

Creating Predicates

Porolog::Predicate.new takes three parameters:

  1. name, which is the name of the predicate
  2. scope, which is the "name" of the logic namespace the predicate is to be defined in
  3. builtin:, which is a boolean (defaulting to false) indicating whether the predicate is to be evaluated using a library definition

The scope does not have to be a String or Symbol; it can be any instance. Thus, separate classes can contribute to the same logic space if desired. If a specific instance is provided, then the predicates are only defined in that instance's logic space. Thus, these commands have different logic space scopes:

class Widget

  def initialize
    predicate1 = Porolog::Predicate.new :my_pred, :Widget
    predicate2 = Porolog::Predicate.new :my_pred, self.class
    predicate3 = Porolog::Predicate.new :my_pred, self
  end

end

Using :Widget defines a logic space, which can be referred to anywhere by using the Symbol. Using self.class also defines a logic space, which can be referred to anywhere by using the Class. These are effectively the same.

The third predicate is scoped to the specific instance. The logic space can be referred to anywhere but needs the instance to refer to it. Thus Ruby name spaces are independent of Porolog name spaces.

The above scopes can be referred to with the following commands:

class Gadget

  def initialize(widget)
    predicate1 = Porolog::Predicate.new :my_pred, :Widget
    predicate2 = Porolog::Predicate.new :my_pred, Widget
    predicate3 = Porolog::Predicate.new :my_pred, widget
  end

end

The Complete Example

This example puts everything together so that namespace pollution is completely avoided.

Note that when providing arguments to predicates, you must now use a dot ., for example:

predciate :my_pred

my_pred(:X).fact!

becomes

my_pred = Porolog::Predicate.new :my_pred

my_pred.(:X).fact!
require 'porolog'

class Numbers

  def initialize(number)
    @number = number
    
    @prime        = Porolog::Predicate.new :prime,        self
    search_prime  = Porolog::Predicate.new :search_prime, self
    gtr           = Porolog::Predicate.new :gtr,          self, builtin: true
    is            = Porolog::Predicate.new :is,           self, builtin: true
    noteq         = Porolog::Predicate.new :noteq,        self, builtin: true
    between       = Porolog::Predicate.new :between,      self, builtin: true
    
    @prime.(2).fact!
    @prime.(3).fact!
    @prime.(:X) << [
      between.(:X, 4, 370),
      is.(:X_mod_2, :X) {|x| x % 2 },
      noteq.(:X_mod_2, 0),
      search_prime.(:X, 3),
    ]

    search_prime.(:X, :N) << [
      is.(:N_squared, :N) {|n| n ** 2 },
      gtr.(:N_squared, :X),
      :CUT
    ]

    search_prime.(:X, :N) << [
      is.(:X_mod_N, :X, :N) {|x,n| x % n },
      noteq.(:X_mod_N, 0),
      is.(:M, :N) {|n| n + 2 },
      :CUT,
      search_prime.(:X, :M),
    ]
  end
  
  def kind
    @prime.(@number).valid? ? :prime : :not_prime
  end
  
  def solutions
    @prime.(:x).solve_for(:x)
  end
  
end

number = Numbers.new 120

puts number.kind.inspect

number = Numbers.new 23

puts number.kind.inspect
puts number.solutions.inspect