Prolog Recursion skipping same results

Tag: recursion , prolog , prolog-setof Author: adibuai Date: 2011-11-12

My code runs but the problem is it shows the same results more than once. Here's my code:

disease(hiv,[sore_throat,headache,fever,rash]).
disease(pregnancy,[fatigue,vomiting,light_headedness,increased_waistline]).
disease(flu,[fatigue,fever,tiredness,nasal_discharge]).

diagnose([], []).
diagnose(Name, [H|T]) :-
    disease(The_Disease, Symptoms),
    member(H, Symptoms),
    write(Name), write(' has/is '), writeln(The_Disease),
    diagnose(Name, T).

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

Result when executed in prolog:

?- diagnose(kevin,[sore_throat,fatigue,tiredness,rash]).
kevin has/is hiv
kevin has/is pregnancy
kevin has/is flu
kevin has/is hiv
kevin has/is flu
kevin has/is flu
kevin has/is hiv
false.

How do I avoid that same results? I tried using other method I found here:

filter_doubles([], []).
filter_doubles([X|L], Result) :-
    (memberchk(X,L) ->
        filter_doubles(L, Result)
    ;
        filter_doubles(L, Result0),
        Result = [X|Result0]
    ).

But I failed to apply it to my code. Help please.

Is it perhaps intended that you check all symptoms of a disease are present before returning that diagnosis?
@hardmath yes that's the plan.

Best Answer

Your program has a pure core - or to stick to medical terms - a pure heart, but this is intertwined with cancerous I/O tissue! In this manner doing it right is very difficult, if not impossible. For example, as a minor error, your program fails for kevin. But you probably meant it to succeed. On the other hand, you will succeed for the mysterious mister []! Who is that?

So lets separate the pure from the impure!

The pure part in your program is about relating a list of symptoms to possible diagnoses. Your working assumption is that if there is one symptom which is part of the indications for a malady, we will diagnose that malady - just to be sure. So why not call this symptoms_diagnosis/2?

symptoms_diagnosis(Symptoms, Diagnosis) :-
   member(Symptom, Symptoms),
   disease(Diagnosis, Indications),
   member(Symptom, Indications).

?- symptoms_diagnosis([sore_throat,fatigue,tiredness,rash], Diagnosis).
Diagnosis = hiv ;
Diagnosis = pregnancy ;
Diagnosis = flu ;
Diagnosis = flu ;
Diagnosis = hiv ;
false.

Note that even without any further ado, we have less redundant solutions than in your original program. So how to get rid of the remaining redundant solutions? This does the trick:

?- setof(t,symptoms_diagnosis([sore_throat,fatigue,tiredness,rash], Diagnosis),_).
Diagnosis = flu ;
Diagnosis = hiv ;
Diagnosis = pregnancy.

So whenever you get redundant solutions, simply wrap a setof(t, ..., _) around your goal. You can use that whenever the answers are ground answers. That is, there is no variable left in the answer.

Maybe you prefer to get the diagnosis in a list of its own?

?- setof(Diagnosis,symptoms_diagnosis([sore_throat,fatigue,tiredness,rash],Diagnosis),Diagnoses).
Diagnoses = [flu, hiv, pregnancy].

So, now we are ready for the Princeton-Plainsboro Teaching Hospital! Its only superstition if Dr. House will not accept Prolog's diagnosis!

For the impure part, please look at @Mog's approach.

comments:

Thank you @false! With your explanation, I'm beginning to understand prolog better. Sorry about the mystery [] where it should be Name instead. Yup I prefer to get the diagnosis in a list of its own.
Welcome! When learning Prolog, try to stay away from side-effectful built-ins as long as possible. It's the pure part of Prolog that makes it so interesting.

Other Answer1

You have to remember what diseases you already collected when checking the symptoms. You need to collect (aggregate) the diseases in a list, and check whether a disease is already present in the list before adding it. You can then print the list at the end or print out each new disease as it is added to the list.

I would implement it this way:

diagnose(Name, Symptoms) :- diagnose0(Name, Symptoms, []).

diagnose0(Name, [], Diseases) :-
    print_diseases(Name, Diseases).
diagnose0(Name, [H|T], DIn) :-
    disease(Disease, Symptoms),
    member(H, Symptoms),
    % you may want to add a cut here to avoid choicepoints
    (
        member(Disease, DIn)
    ->
        diagnose0(Name, T, DIn)
    ;
        DOut = [Disease|DIn],
        diagnose0(Name, T, DOut)
    ).

print_diseases(_Name, []).
print_diseases(Name, [H|T]) :-
    write(Name), write(' has/is '), writeln(H),
    print_diseases(Name, T).

The disease/2 facts are as in your code.

This gives:

?- diagnose(kevin, [sore_throat, fatigue, tiredness, rash]).
kevin has/is flu
kevin has/is pregnancy
kevin has/is hiv
Yes (0.00s cpu, solution 1, maybe more)

The next step then would obviously be to find a way to express that some diagnoses represent alternatives for a given symptom, and choose between these different alternatives. With the symptoms listed in the query, Kevin should have flu and HIV, but I doubt that pregnancy is a correct diagnosis for Kevin. This is related to the comment about a cut that I inserted in the second clause of diagnose/3: without the cut, you can get more than one solution, each representing a different set of diseases that match the set of symptoms. If you add a cut, you only get the first solution (including pregnancy). The second solution only contains flu and HIV.

BTW, member/2 is a built-in predicate, so you don't need to define your own.

Other Answer2

Alternatively, you can write :

disease(hiv,[sore_throat,headache,fever,rash]).
disease(pregnancy,[fatigue,vomiting,light_headedness,increased_waistline]).
disease(flu,[fatigue,fever,tiredness,nasal_discharge]).

diagnose(Name, Symptoms) :-
    findall(D, (disease(D, S), intersection(S, Symptoms, I), I \== []), MayGot),
    atomic_concat(Name, ' has/is ', Start),
    maplist(atomic_concat(Start), MayGot, Temp),
    maplist(writeln, Temp).

But if you're trying to learn Prolog it's not a good idea since it's more functionnal and less Prolog-ish, thought I'd mention this possibility anyway !

comments:

Very nice! I find maplist/2 and maplist/3 very Prolog-ish as well - to make the code more Prolog-ish, a minor suggestion: Instead of disease/2, we could call the predicate for example disease_symptoms/2 to be more descriptive.
@Mog It is much better to perform I/O as you do and not the "traditional" way in failure driven loops! In this manner errors in the I/O part will turn out as unintended failure.
To mat : I tried not to rename OP's facts but I do agree that disease_symptoms would be more readable ! To false : thanks for poiting that out !
@Mog thanks alot! Your solution works perfect! i will need to understand all the findall/3 and maplist/2 and maplist/3 before I can proceed further.
Don't hesitate to ask further questions if you struggle because documentation is hard to find sometimes !