Custom Predicates - wizardofosmium/porolog GitHub Wiki

Custom Predicates

The aim of Porolog is not to provide an interface to a Prolog instance but rather to provide the power of declarative logic to Ruby as though it were a part of Ruby, and thus allow intermixing of declarative logic in the same way that functional programming can be intermixed in Ruby.

There are three main levels of adding Ruby into the logic:

  • in an is predicate call,
  • in a ruby predicate call, and
  • in a custom builtin predicate.

Ruby Code in an is

If you want the result of a Ruby block to be inserted into the logic, you can use an is predicate call. The result is instantiated to the target variable. If the variable is already instantiated, then the result is unified with the variable, and thus the goal may fail if the values are incompatible.

builtin :is
predicate :get_ruby_result

get_ruby_result(:X) << [
  is(:X) {
    # ... Ruby code here ...
  }
]

See: https://github.com/wizardofosmium/porolog/wiki/Builtin-Predicates#isvariable-args-is_block for more details on is.

Ruby Code in a ruby

If you are not specifically wanting a result from Ruby but rather just some code to be executed, then you can use a ruby predicate call instead of an is predicate call.

builtin :ruby
predicate :do_ruby_actions

do_ruby_actions() << [
  ruby() {
    # ... Ruby code here ...
  }
]

See: https://github.com/wizardofosmium/porolog/wiki/Builtin-Predicates#rubyargs-ruby_block for more details on ruby.

Ruby Code in a Custom Builtin Predicate

If you want your code DRYer, you could simply use a method call to DRY up your code. However, if it is more convenient to have your code as a custom builtin predicate, you can do that by declaring a method as a builtin predicate.

You need to define the method in Porolog::Predicate::Builtin. The first two parameters need to be:

  1. goal, which is the current goal
  2. block, which is the logic stack

For the logic tree to be continued to be traversed, you need to call the block if the predicate has been satisfied and return its result, or return false if the predicate has not been satisfied. Arguments and also a block can be provided to the custom predicate if desired. The goal and block parameters are required but you can otherwise define the parameters as a normal Ruby method. The custom builtin predicate can now be part of the backward chaining process.

module Porolog
  class Predicate
    module Builtin
      def custom(goal, block, *args, &arg_block)
        # ... Ruby code here ...
        block.call(goal) || false
      end
    end
  end
end

Quadratic Solver

This is an example of using the quadratic equation as a custom builtin.

require 'porolog'

include Porolog

module Porolog
  class Predicate
    module Builtin
      def quadratic(goal, block, x, a, b, c)
        a = a.value.value
        b = b.value.value
        c = c.value.value
        
        return false if [a,b,c].any?{|v| v.type == :variable }
        return false unless x.type == :variable
        
        satisfied = false
        solutions = []
        
        if a != 0
          d = b ** 2 - 4 * a * c
          if d == 0
            solutions << (-b / 2 / a)
          else
            if d > 0
              solutions << ((-b - Math.sqrt(d)) / 2 / a)
              solutions << ((-b + Math.sqrt(d)) / 2 / a)
            end
          end
        end
        
        solutions.each do |solution|
          instantiation = x.instantiate(solution)
          instantiation && block.call(goal) && (satisfied = true)
          instantiation&.remove
          return satisfied if goal.terminated?
        end
        
        satisfied
      end
    end
  end
end

builtin :quadratic, :gtr
predicate :use_quadratic, :positive_solutions

use_quadratic(:X, :A, :B, :C) << [
  quadratic(:X, :A, :B, :C)
]

positive_solutions(:X, :A, :B, :C) << [
  quadratic(:X, :A, :B, :C),
  gtr(:X, 0)
]

puts use_quadratic(:X, 1, 1, 12).solve.inspect
puts use_quadratic(:X, 1, 1, -12).solve.inspect
puts use_quadratic(:X, 2, 4, 2).solve.inspect
puts use_quadratic(-1, 2, 4, 2).solve.inspect
puts use_quadratic(1, 2, 4, 2).solve.inspect
puts use_quadratic(3, 1, 1, -12).solve.inspect

puts positive_solutions(:X, 1, 1, -12).solve.inspect

Combinations

This is an example of defining a custom builtin predicate, similar to permutation that returns combinations instead of permutations.

require 'porolog'

include Porolog

module Porolog
  class Predicate
    module Builtin
      def combination(goal, block, list, size, combination)
        list = list.value.value
        size = size.value.value
        
        return false unless list.type == :array
        return false unless size.is_a?(Integer)
        return false unless combination.type == :variable
        
        satisfied = false
        
        list.combination(size).each do |array|
          instantiation = combination.instantiate(array)
          instantiation && block.call(goal) && (satisfied = true)
          instantiation&.remove
          return satisfied if goal.terminated?
        end
        
        satisfied
      end
    end
  end
end

builtin :combination, :length, :between
predicate :use_combination, :subset

use_combination(:List, :Size, :Combination) << [
  combination(:List, :Size, :Combination)
]

subset(:Subset, :Set) << [
  length(:Set, :Size),
  between(:Subset_size, 0, :Size),
  combination(:Set, :Subset_size, :Subset)
]

puts use_combination([1,2,3,4,5], 3, :C).solve.inspect
puts subset(:Subset, [1,2,3,4,5]).solve.inspect