Search code examples
prolog

Prolog - How To Make Prolog Query Shorter?


I've been trying to learn Prolog and this particular thing has been bugging me for a while: to get anything done, my query need to be shockingly long. In particular, I have the following snippet that represents a simple classification program:

% Define the objects
object(pencil, stationery).
object(apple, organic).
object(eraser, stationery).
object(book, stationery).
object(mouse, electronic).
object(mp3, electronic).
object(ipad, electronic).

% Define the baskets
basket(red_basket, electronic).
basket(tall_basket, stationery).
basket(round_basket, organic).

% Define the sorting rule
sort_object(Object, Basket) :-
    object(Object, Type),
    basket(Basket, Type).

% Query the program to sort the objects into the baskets
% How can we reduce this last statement to be MUCH shorter?
?- sort_object(pencil, Basket1), sort_object(apple, Basket2), sort_object(eraser, Basket3), sort_object(book, Basket4), sort_object(mouse, Basket5), sort_object(mp3, Basket6), sort_object(ipad, Basket7).

I like it that all the term definitions are as succinct as they can be, but the final query is WAYYYYYYY TOOOO LONG to my liking and feels wrong. In contrary, if we were to implement the same thing in Elixir, one might do:

defmodule DB do
  def objects do
    [:pencil, :iPad, :basketball, :laptop, :apple]
  end
end

defmodule ObjectHelper do
  def get_type(:pencil), do: :stationery
  def get_type(:iPad), do: :electronics
  def get_type(:basketball), do: :sport
  def get_type(:laptop), do: :electronics
  def get_type(:apple), do: :food
end

defmodule BasketHelper do
  def get_basket(:electronics), do: :red
  def get_basket(:stationery), do: :yellow
  def get_basket(_), do: :blue
end

DB.objects
|> Enum.group_by(fn o -> o |> ObjectHelper.get_type |> BasketHelper.get_basket end)
|> IO.inspect

The goal is to output the correct baskets for each object, and you can envision doing that easily in C++ or Python cleanly albeit with longer code.

My question is:

  1. Is there any construct to allow a more succinct query?
  2. If there anything "wrong" with my question - maybe for achieving this task in practice, there are better ways to formulate the problem?

I am using SWI Prolog.
Thanks!


Solution

  • First option is findall/3 (or bagof/3) to find all solutions to a goal and gather up the results according to a template. For this, an Object and a Basket are a pair of things and a common way to pair two things in Prolog is A-B, a term of two things joined with a hyphen:

    ?- findall(O-B, sort_object(O, B), Pairs).
    
    Pairs = [pencil-tall_basket, apple-round_basket, eraser-tall_basket, 
             book-tall_basket, mouse-red_basket, mp3-red_basket, ipad-red_basket]
    

    Second option is a less pure 'failure driven loop' to search, print the result, then fail the search and trigger backtracking to retry the search:

    ?- sort_object(O, B),
       format('Thing: ~w, Basket: ~w,~n', [O,B]),
       false.
    
    Thing: pencil. Basket: tall_basket.
    Thing: apple. Basket: round_basket.
    Thing: eraser. Basket: tall_basket.
    Thing: book. Basket: tall_basket.
    Thing: mouse. Basket: red_basket.
    Thing: mp3. Basket: red_basket.
    Thing: ipad. Basket: red_basket.
    false
    

    but that doesn't give you the items to work with in your program.

    Third option if you want the items grouped, SWI Prolog has group_by/4 which finds the groups on backtracking:

    ?- group_by(B, O, sort_object(O, B), Items).
    
    B = red_basket,
    Items = [mouse, mp3, ipad] ;
    
    B = round_basket,
    Items = [apple] ;
    
    B = tall_basket,
    Items = [pencil, eraser, book]
    

    and that can be wrapped in findall as well:

    ?- findall(B-Items,
            group_by(B, O, sort_object(O, B), Items),
            AllGroups).
    
    AllGroups = [red_basket-[mouse, mp3, ipad],
                 round_basket-[apple],
                 tall_basket-[pencil, eraser, book]]