0
votes

I want to ask pros and cons of different Prolog representations in arguments of predicates.

For example in Exercise 4.3: Write a predicate second(X,List) which checks whether X is the second element of List. The solution can be:

second(X,List):- [_,X|_]=List.

Or,

second(X,[_,X|_]).

The both predicates would behave similarly. The first one would be more readable than the second, at least to me. But the second one uses more stacks during the execution (I checked this with trace).

A more complicated example is Exercise 3.5: Binary trees are trees where all internal nodes have exactly two children. The smallest binary trees consist of only one leaf node. We will represent leaf nodes as leaf(Label) . For instance, leaf(3) and leaf(7) are leaf nodes, and therefore small binary trees. Given two binary trees B1 and B2 we can combine them into one binary tree using the functor tree/2 as follows: tree(B1,B2) . So, from the leaves leaf(1) and leaf(2) we can build the binary tree tree(leaf(1),leaf(2)) . And from the binary trees tree(leaf(1),leaf(2)) and leaf(4) we can build the binary tree tree(tree(leaf(1), leaf(2)),leaf(4)). Now, define a predicate swap/2 , which produces the mirror image of the binary tree that is its first argument. The solution would be:

A2.1:

swap(T1,T2):- T1=tree(leaf(L1),leaf(L2)), T2=tree(leaf(L2),leaf(L1)).
swap(T1,T2):- T1=tree(tree(B1,B2),leaf(L3)), T2=tree(leaf(L3),T3), swap(tree(B1,B2),T3).
swap(T1,T2):- T1=tree(leaf(L1),tree(B2,B3)), T2=tree(T3,leaf(L1)), swap(tree(B2,B3),T3).
swap(T1,T2):- T1=tree(tree(B1,B2),tree(B3,B4)), T2=tree(T4,T3), swap(tree(B1,B2),T3),swap(tree(B3,B4),T4).

Alternatively,

A2.2:

swap(tree(leaf(L1),leaf(L2)), tree(leaf(L2),leaf(L1))).
swap(tree(tree(B1,B2),leaf(L3)), tree(leaf(L3),T3)):- swap(tree(B1,B2),T3).
swap(tree(leaf(L1),tree(B2,B3)), tree(T3,leaf(L1))):- swap(tree(B2,B3),T3).
swap(tree(tree(B1,B2),tree(B3,B4)), tree(T4,T3)):- swap(tree(B1,B2),T3),swap(tree(B3,B4),T4).

The number of steps of the second solution was much less than the first one (again, I checked with trace). But regarding the readability, the first one would be easier to understand, I think.

Probably the readability depends on the level of one's Prolog skill. I am a learner level of Prolog, and am used to programming with C++, Python, etc. So I wonder if skillful Prolog programmers agree with the above readability.

Also, I wonder if the number of steps can be a good measurement of the computational efficiency.

Could you give me your opinions or guidelines to design predicate arguments?


EDITED.

According to the advice from @coder, I made a third version that consists of a single rule:

A2.3:

swap(T1,T2):-
    ( T1=tree(leaf(L1),leaf(L2)), T2=tree(leaf(L2),leaf(L1)) );
    ( T1=tree(tree(B1,B2),leaf(L3)), T2=tree(leaf(L3),T3), swap(tree(B1,B2),T3) );
    ( T1=tree(leaf(L1),tree(B2,B3)), T2=tree(T3,leaf(L1)), swap(tree(B2,B3),T3) );
    ( T1=tree(tree(B1,B2),tree(B3,B4)), T2=tree(T4,T3), swap(tree(B1,B2),T3),swap(tree(B3,B4),T4) ).

I compared the number of steps in trace of each solution:

  • A2.1: 36 steps
  • A2.2: 8 steps
  • A2.3: 32 steps

A2.3 (readable single-rule version) seems to be better than A2.1 (readable four-rule version), but A2.2 (non-readable four-rule version) still outperforms.

I'm not sure if the number of steps in trace is reflecting the actual computation efficiency. There are less steps in A2.2 but it uses more computation cost in pattern matching of the arguments. So, I compared the execution time for 40000 queries (each query is a complicated one, swap(tree(tree(tree(tree(leaf(3),leaf(4)),leaf(5)),tree(tree(tree(tree(leaf(3),leaf(4)),leaf(5)),leaf(4)),leaf(5))),tree(tree(leaf(3),tree(tree(leaf(3),leaf(4)),leaf(5))),tree(tree(tree(tree(leaf(3),leaf(4)),leaf(5)),leaf(4)),leaf(5)))), _). ). The results were almost the same (0.954 sec, 0.944 sec, 0.960 sec respectively). This is showing that the three reresentations A2.1, A2.2, A2.3 have close computational efficiency. Do you agree with this result? (Probably this is a case specific; I need to vary the experimental setup).

2
Flagged as opinion-based.takendarkk
Regarding readability, I would first focus on the name of the predicate itself. For example, suppose I tell you I have a predicate second/2, and it describes a relation between a list and its second element. But which argument is the list? The first or the second one? On the other hand, suppose I have a predicate called list_second_element/2, or list_second/2, then it is completely clear which argument denotes the list. Similarly, what are the arguments of swap/2? Start with tree_tree/2, and then find more specific names denoting what each argument means.mat
@mat I agree. Predicate naming should be informative.Akihiko
"Readable" means very little, since it is quite subjective. As long as you are used to a certain style, you will find it more readable. In the same way, if you read enough idiomatic Prolog, you will start to appreciate it and find it more readable than say C-style Prolog. You could also argue that C idioms like while ((c = getchar()) != EOF) are utter nonsense, but after seeing it often enough it becomes, well, an idiom.user1812457
@Boris Definitely "readability" is subjective. As I wrote above, I am a beginner of Prolog. But readability is an important factor in writing program. Tell me your feeling w.r.t. readability as well as your experience of Prolog. It's beneficial to me.Akihiko

2 Answers

4
votes

This question is a very good example of a bad question for a forum like Stackoverflow. I am writing an answer because I feel you might use some advice, which, again, is very subjective. I wouldn't be surprised if the question gets closed as "opinion based". But first, an opinion on the exercises and the solutions:

Second element of list

Definitely, second(X, [_,X|_]). is to be preferred. It just looks more familiar. But you should be using the standard library anyway: nth1(2, List, Element).

Mirroring a binary tree

The tree representation that the textbook suggests is a bit... unorthodox? A binary tree is almost invariably represented as a nested term, using two functors, for example:

  1. t/3 which is a non-empty tree, with t(Value_at_node, Left_subtree, Right_subtree)
  2. nil/0 which is an empty tree

Here are some binary trees:

  • The empty tree: nil
  • A binary search tree holding {1,2,3}: t(2, t(1, nil, nil), t(3, nil, nil))
  • A degenerate left-leaning binary tree holding the list [1,2,3] (if you traversed it pre-order): t(1, t(2, t(3, nil, nil), nil), nil)

So, to "mirror" a tree, you would write:

mirror(nil, nil).
mirror(t(X, L, R), t(X, MR, ML)) :-
    mirror(L, ML),
    mirror(R, MR).

The empty tree, mirrored, is the empty tree. A non-empty tree, mirrored, has its left and right sub-trees swapped, and mirrored.

That's all. No need for swapping, really, or anything else. It is also efficient: for any argument, only one of the two clauses will be evaluated because the first arguments are different functors, nil/0 and t/3 (Look-up "first argument indexing" for more information on this). If you would have instead written:

mirror_x(T, MT) :-
    (   T = nil
    ->  MT = nil
    ;   T = t(X, L, R),
        MT = t(X, MR, ML),
        mirror_x(L, ML),
        mirror_x(R, MR)
    ).

Than not only is this less readable (well...) but probably less efficient, too.

On readability and efficiency

Code is read by people and evaluated by machines. If you want to write readable code, you still might want to address it to other programmers and not to the machines that are going to evaluate it. Prolog implementations have gotten better and better at being efficient at evaluating code that is also more readable to people who have read and written a lot of Prolog code (do you recognize the feedback loop?). You might want to take a look at Coding Guidelines for Prolog if you are really interested in readability.

A first step towards getting used to Prolog is trying to solve the 99 Prolog Problems (there are other sites with the same content). Follow the suggestion to avoid using built-ins. Then, look at the solutions and study them. Then, study the documentation of a Prolog implementation to see how much of these problems have been solved with built-in predicates or standard libraries. Then, study the implementations. You might find some real gems there: one of my favorite examples is the library definition of nth0/3. Just look at this beauty ;-).

There is also a whole book written on the subject of good Prolog code: "The Craft of Prolog" by Richard O'Keefe. The efficiency measurements are quite outdated though. Basically, if you want to know how efficient your code is, you end up with a matrix with at least three dimensions:

  • Prolog implementation (SWI-Prolog, SICSTUS, YAP, Gnu-Prolog...)
  • Data structure and algorithm used
  • Facilities provided by the implementation

You will end up having some wholes in the matrix. Example: what is the best way to read line-based input, do something with each line, and output it? Read line by line, do the thing, output? Read all at once, do everything in memory, output at once? Use a DCG? In SWI-Prolog, since version 7, you can do:

read_string(In_stream, _, Input),
split_string(Input, "\n", "", Lines),
maplist(do_x, Lines, Xs),
atomics_to_string(Xs, "\n", Output),
format(Out_stream, "~s\n", Output)

This is concise and very efficient. Caveats:

  • The available memory might be a bottle neck
  • Strings are not standard Prolog, so you are stuck with implementations that have them

This is a very basic example, but it demonstrates at least the following difficulties in answering your question:

  • Differences between implementations
  • Opinions on what is readable or idiomatic Prolog
  • Opinions on the importance of standards

The example above doesn't even go into details about your problem, as for example what you do with each line. Is it just text? Do you need to parse the lines? Why are you not using a stream of Prolog terms instead? and so on.

On efficiency measurements

Don't use the number of steps in the tracer, or even the reported number of inferences. You really need to measure time, with a realistic input. Sorting with sort/2, for example, always counts as exactly one inference, no matter what is the length of the list being sorted. On the other hand, sort/2 in any Prolog is about as efficient as a sort on your machine would ever get, so is that an issue? You can't know until you have measured the performance.

And of course, as long as you make an informed choice of an algorithm and a data structure, you can at the very least know the complexity of your solution. Doing an efficiency measurement is interesting only if you notice a discrepancy between what you expect and what you measure: obviously, there is a mistake. Either your complexity analysis is wrong, or your implementation is wrong, or even the Prolog implementation you are using is doing something unexpected.

On top of this, there is the inherent problem of high-level libraries. With some of the more complex approaches, you might not be able to easily judge what the complexity of a given solution might be (constraint logic programming, as in CHR and CLPFD, is a prime example). Most real problems that fit nicely to the approach will be much easier to write, and more efficient than you could ever do without considerable effort and very specific code. But get fancy enough, and your CHR program might not even want to compile any more.

Unification in the head of the predicate

This is not opinion-based any more. Just do the unifications in the head if you can. It is more readable to a Prolog programmer, and it is more efficient.

PS

"Learn Prolog Now!" is a good starting point, but nothing more. Just work your way through it and move on.

1
votes

In the first way for example for Exercise 3.5 you use the rule swap(T1,T2) four times ,which means that prolog will examine all these four rules and will return true or fail for every of these four calls .Because these rules can't all be true together (each time one of them will return true) ,for every input you waste three calls that will not succeed (that's why it demands more steps and more time ). The only advantage in the above case is that by writing with the first way ,it is more readable. In generally when you have such cases of pattern matching it's better to write the rules in a way that are well defined and not two(or more) rules match a input ,if of course you require only one answer ,as for example the second way of writing the above example . Finally one example where it is required that more than one rules match an input is the predicate member where it is written:

member(H,[H|_]).
member(H,[_|T]):- member(H,T). 

where in this case you require more than one answers.

In the third way you just write the first way without pattern matching .It has the form (condition1);...;(condition4) and if the condition1 does not return true it examines the next condition .Most of the times the fourth condition returns true ,but it has called and tested condition1-3 which returned false .So it is almost as the first way of writing the solution ,except the fact that in third solution if it finds true condition1 it will not test other conditions so you will save some wasted calls (compared to solution1). As for the running time ,it was expected to be almost the same because in worst case solution 1 and 3 does four times the tests/calls that solution 2 does .So if solution2 is O(g) complexity (for some function g) ,then solution 1 and 3 are O(4g) which is O(g) complexity so running times will be very close.