1
votes

Learning some basic prolog and am having difficulty wrapping my head around the logic.

Scenario: An individual contracts viral meningitis and happens to interact with various other individuals. This is my prolog logic so far.

%-- Set a sickness condition.
%-- -------------------------------------------------------- --%
setILL(X) :- write(X), write(' is ill'), nl.

%-- Link some interactions between individuals.
%-- -------------------------------------------------------- --%
interact(ella, james).
interact(ella, tyrone).
interact(james, ben).
interact(james, frank).
interact(james, carl).
interact(carl, james).
interact(carl, evan).
interact(evan, mike).
interact(evan, kelly).
interact(mike, frank).
interact(kelly, carl).
interact(kelly, frank).
interact(kelly, ben).
interact(sven, mike).

%-- Create an interaction condition.
%-- -------------------------------------------------------- --%
came_in_contact(X, Y) :- setILL(X), write(X), write(' has had contact with '), write(Y), X\=Y, !, nl.  

%-- Create a rule for sickness
%-- -------------------------------------------------------- --%
sick(X) :- interact(X, Y), contact(X, Y), Y\=X.  
whosick(R) :- findall([X], sick(X), R).

Now the interactions are what they are supposed to be and there should be two paths each start with ella (whom is originally sick) and ends with sven (the supposed last individual to be sick). I simply wish to print out the two possible paths without including useless interactions. E.g. tyrone will talk to no one else and neither will ben. I also wish to remove repeasts (see below).

When I execute

whosick(X).

I get

ella is ill
ella has had contact with james
ella is ill
ella has had contact with tyrone
james is ill
james has had contact with ben
james is ill
james has had contact with frank
james is ill
james has had contact with carl
carl is ill
carl has had contact with james
carl is ill
carl has had contact with evan
evan is ill
evan has had contact with mike
evan is ill
evan has had contact with kelly
mike is ill
mike has had contact with frank
kelly is ill
kelly has had contact with carl
kelly is ill
kelly has had contact with frank
kelly is ill
kelly has had contact with ben
sven is ill
sven has had contact with mike
X = [[ella], [ella], [james], [james], [james], [carl], [carl], [evan], [...]|...].
1

1 Answers

2
votes

First of all, there's a typo in the code you provided: came_in_contact should be contact or it won't run. Minor issue.

Second issue: I'm not sure what you mean by this: findall([X], sick(X), R). There's no especial reason to use [X] here instead of just X, and the results look a little nicer with this change:

X = [ella, ella, james, james, james, carl, carl|...].

An issue with more significance is that, stylistically, setill is 100% side-effects despite the stateful-sounding name. setill doesn't "set" anyone "ill," it just prints to standard output that someone is ill. It is, you might say, part of the "view" if this were MVC. And so part of the problem you're having is that you're calling this "view" code from deep within the "model," in sick/2.

You mention parenthetically that Ella is the origin of the outbreak, but there's no fact in your Prolog database, so Prolog is certainly not aware of it. Additionally, you seem to be interested in the "path" that infection took, but your Prolog doesn't know anything about a path—in fact, it's really just dumping out your fact database. To prove it, let's add a new fact at the top:

interact(gail, hank).

Sure enough, it's now the first "solution" even though Gail and Hank are isolated from the rest of the graph:

gail is ill
gail has had contact with hank
… (old output repeated)

...

So, you're kind of off in the weeds here. You have an incomplete fact database, your rules don't really capture the logic of the problem and they intersperse logic with printing. By the time we're done here the code is going to look pretty different. I'm not sure whether this is homework or not, it sounds like you're self-studying, but it has kind of a homeworky vibe so I'm going to try and sketch out how I would proceed without putting it all together.


First, you need to make Prolog aware of all the facts it needs to compute the solution. To wit, you have to add a fact about the originator:

infected(ella) :- !.

This is going to become the base case. Now we need to employ inductive reasoning and say, a person is infected if that person has been in contact with an infected person:

infected(X) :- interact(X, Y), X \= Y, infected(Y), !.

Note: these cuts are fairly important. There's no need to compute another solution because a person either is or is not infected. If we succeed on either branch proving they are infected, there's nothing else to say.

Now we can get reasonable solutions for some people:

?- infected(ella).
true.

?- infected(gail).
false.

Other people seem to get no solution:

?- infected(james).
(I typed Ctrl+C)
^CAction (h for help) ? abort
% Execution Aborted

The reason James doesn't arrive at a solution is because Prolog is using a depth-first search. Handily, the next thing you have to do is discover the infection path, so if you can prevent Prolog from trying people who are already in the path, you can solve the problem by getting the path you also need. You're going to have to do employ a similar base case/inductive case structure, but pass along an additional argument for the path of the infection. You can find examples of this kind of thing all over so I won't bore you with the details here.

Do take note of this: we are not going to intermix the logic of the problem with the displaying of results. This is just good policy with Prolog because of backtracking. If you print something out because a binding succeeded here, and in the next term it fails, the whole failure could go back past the printout, leaving a confused user. We can easily trick Prolog into printing out lies from solutions that failed later on. So you always want to write your Prolog so that it finds the solutions and then displays them separately. Think model-view-controller.

So let's assume you have found a predicate, path/3 (presumably path(Source, Last, Path)). When you run it, you're going to get solutions like this:

?- path(ella, X, Path).
X = sven
Path = [ella, james, ...] ;

X = sven
Path = [ella, tyrone, ...] ;
false.

This is the predicate you'll want to wrap with your findall/3, and then you'll want to walk through the results and print out the parts you need path-by-path.

Edit: In response to your comment, let's take a look at your new predicate:

path(_, X, P) :- findall(X, interact(_, X), P).

I'm afraid this isn't any closer than before. Let's see what happens when I ask for the path from myself:

?- path('Daniel Lyons', X, Path).
Path = [james, tyrone, ben, frank, carl, james, evan, mike|...].

In fact you can put absolutely anything in there and you'll get exactly the same result:

?- path('Jack Donaghy', X, Path).
Path = [james, tyrone, ben, frank, carl, james, evan, mike|...].
?- path(3.1415926, X, Path).
Path = [james, tyrone, ben, frank, carl, james, evan, mike|...].
?- path([a,b,c,d,e], X, Path).
Path = [james, tyrone, ben, frank, carl, james, evan, mike|...].

This is because your rule is true for anything in the first position. If you had more clauses this could be meaningful because one of the other clauses could say something about this argument, but lacking that it really does mean anything. So your predicate could just as well be written:

path(X, P) :- findall(X, interact(_, X), P).

Every _ is a completely unique binding; they do not influence each other at all, so if you were hoping for an effect there you'd want something more like this:

path(F, X, P) :- findall(X, interact(F, X), P).

And you see right away that this doesn't help you much:

?- path(ella, X, P).
P = [james, tyrone].

So let's solve the problem already.

person(X) :- interact(X, _) ; interact(_, X).

This is just a helper that returns everybody whether they're on the left or right of the interaction.

path(Originator, Path) :- 
  setof(X, person(X), People), 
  path(Originator, Path, People).

This helper gets you a path from a particular person. We are relying on a helper function I'll show in just a second. We prune the possibilities to something reasonable by starting off with the list of all people. That way we can just choose the next person from the list of people we haven't examined yet, and we don't have to worry about cycles or too much recursion.

path(Originator, [], _).
path(Originator, [NextPerson|Rest], Considering) :-
  select(NextPerson, Considering, RemainingToConsider),
  interact(Originator, NextPerson), 
  path(NextPerson, Rest, RemainingToConsider).

The first clause says, we can always be done. The path from the originator to nobody is the empty path. This is a base case for our induction.

The second clause says, choose someone from the list of people we have left to consider. That someone interacted with the originator. Now find a path from that person through the people remaining to be considered. (select/3 unifies the third argument with the second argument without the first argument).

Let's look at a run:

?- path(ella, X).
X = [] ;
X = [james] ;
X = [james, ben] ;
X = [james, carl] ;
X = [james, carl, evan] ;
X = [james, carl, evan, kelly] ;
X = [james, carl, evan, kelly, ben] ;
X = [james, carl, evan, kelly, frank] ;
X = [james, carl, evan, mike] ;
X = [james, carl, evan, mike, frank] ;
X = [james, frank] ;
X = [tyrone] ;
false.

Now, in your original question you said something about Ben and Frank and not being interested in the other paths. I still don't see a logical reading that will distinguish between those cases, but you can at least find all the longest paths, like this:

longest_paths(Originator, Path) :-
  path(Originator, Path),
  \+ (path(Originator, Path2), 
      length(Path, MaxLen), 
      length(Path2, NextLen), 
      NextLen > MaxLen).

This isn't terribly efficient, but what it says is, find me a path Path from Originator, such that there is no other path with a longer length. This finds us three solutions:

?- longest_paths(ella, X).
X = [james, carl, evan, kelly, ben] ;
X = [james, carl, evan, kelly, frank] ;
X = [james, carl, evan, mike, frank] ;
false.

And this is as close as I think I can get you to the solution you want. I hope it helps!