1

I have a database of facts that has entries like this snippet

symptom(shingles,headache).    
symptom(shingles,fever).    
symptom(shingles,malaise).    
symptom(shingles,headache).    
symptom(shingles,itching).    
symptom(shingles,hyperesthesia).    
symptom(shingles,paresthesia).   

test(shingles,blood).    
test(shingles,pcr).   

locale(shingles,all).  

treatment(shingles,calamine).    
treatment(shingles,aciclovir).

treatment(shingles,valaciclovir).    
treatment(shingles,famciclovir).    
treatment(shingles,corticosteroids).

I then have a predicate that gets a list of symptoms from the user.

getSymptoms(Symptoms) :-
    write('Please enter symptoms now, enter "Done" when finished: ' ),
    read_string(user, "\n", "\r", _, Response),
    (
        Response == "Done"
    ->
        Symptoms = []
    ;
        getSymptoms(Symptoms0),
        Symptoms = [Response|Symptoms0]
    ).

My question is how do I compare the lists of user symptoms to the symptoms fact second atom, and then add the disease to another list? For example, the user enters fever. Since fever is in the symptom fact for shingles, it would add shingles to a list.

false
  • 10,264
  • 13
  • 101
  • 209
  • Related question: [How to Write User Input to a List Prolog](https://stackoverflow.com/q/54677190/1243762). – Guy Coder Feb 15 '19 at 20:44
  • @GuyCoder looking at that now. Any examples you can provide will help because this is my first time using prolog and I need to learn it. – user10876930 Feb 15 '19 at 20:47

1 Answers1

1

This will work, but allows for duplicate symptoms to be entered. I am posting it now so you can see the first part of the transformation and will post the part without the duplicates when I get that working.

getSymptoms(Symptoms) :-
    write('Please enter symptoms now, enter "Done" when finished: ' ),
    read_string(user, "\n", "\r", _, Response),
    (
        Response == "Done"
    ->
        Symptoms = []
    ;
        atom_string(Symptom,Response),
        valid_symptom(Symptom,Symptoms)
    ).

valid_symptom(Symptom,Symptoms) :-
    (
        symptom(_,Symptom)
    ->
        % Symptom was valid
        % so get next symptom and
        % add to list on backtracking
        getSymptoms(Symptoms0),
        Symptoms = [Symptom|Symptoms0]
    ;
        % Symptom was invalid
        % so warn user of invalid symptom and what they input
        % and get next symptom.
        % Do not add invalid Symptom to list on backtracking.
        format('Invalid symptom: `~w''~n',[Symptom]),
        getSymptoms(Symptoms0),
        Symptoms = Symptoms0
    ).

Since the values entered are strings and the symptoms are atoms in symptom/2 facts, the input needs to be converted to an atom for comparison. This is done using atom_string/2

atom_string(Symptom,Response) 

To give a user feedback if the symptom is invalid format/2 is used. It is better to use than write/1 since it gives you more control over the output.

format('Invalid symptom: `~w''~n',[Symptom])

If an invalid value is entered for as a symptom it should not be added to the list. This is a classic if/then type of scenario and in Prolog is done with ->/2. Here is the standard template

(
    <conditional>
->
    <true branch>
;
    <false branch>
)

the conditional is

symptom(_,Symptom)

Notice also with the conditional, that it reads the symptom/2 facts and ignores the first part of the compound structure, i.e. _, and matches the input symptom with a symptom in the facts. This is were the comparison is done, but it is done via unification and not with a compare predicate such as ==/2.

The true branch is the same as before

getSymptoms(Symptoms0),
Symptoms = [Symptom|Symptoms0]

however the false branch is

format('Invalid symptom: `~w''~n',[Symptom]),
getSymptoms(Symptoms0),
Symptoms = Symptoms0

Notice that the invalid Symptom is not added to the list with [Symptom|Symptoms0] and also that both branches (true and false) should update the same variables, Symptoms, which in the false branch is done with Symptoms = Symptoms0 which is not assignment but =/2 (unification).

The code for valid_symptom/2 could have been inlined with getSymptoms/1 but I pulled it out so you could see how it is done in case you need to do that in the future.

Example run:

?- getSymptoms(Symptoms).
Please enter symptoms now, enter "Done" when finished: wrong
Invalid symptom: `wrong'
Please enter symptoms now, enter "Done" when finished: headache
Please enter symptoms now, enter "Done" when finished: malaise
Please enter symptoms now, enter "Done" when finished: headache
Please enter symptoms now, enter "Done" when finished: Done
Symptoms = [headache, malaise, headache].

Here is the next variation that removes duplicates while building the list.

getSymptoms(Result) :-
    getSymptoms_helper([],Result).

getSymptoms_helper(Symptoms,Result) :-
    write('Please enter symptoms now, enter "Done" when finished: ' ),
    read_string(user, "\n", "\r", _, Response),
    (
        Response == "Done"
    ->
        Result = Symptoms
    ;
        atom_string(Symptom,Response),
        valid_symptom(Symptom,Symptoms,Result)
    ).

valid_symptom(Symptom,Symptoms,Result) :-
    (
        memberchk(Symptom,Symptoms)
    ->
        % Symptom was a duplicate
        % Do not add duplicate Symptom to list.
        getSymptoms_helper(Symptoms,Result)
    ;
        (
            symptom(_,Symptom)
        ->
            % Symptom was valid
            % so get next symptom and
            % add to list.
            getSymptoms_helper([Symptom|Symptoms],Result)
        ;
            % Symptom was invalid
            % so warn user of invalid symptom and what they input
            % and get next symptom.
            % Do not add invalid Symptom to list.
            format('Invalid symptom: `~w''~n',[Symptom]),
            getSymptoms_helper(Symptoms,Result)
        )
    ).

The major change here is that an accumulator Symptoms is threaded through the predicates so that the list of valid symptoms can be built and used for testing the next input value. Since the accumulator needs to be initialized at the start, the previous predicate renamed to getSymptoms_helper so that the accumulator can be initialized with

getSymptoms_helper([],Result)

Notice [] that passes to

getSymptoms_helper(Symptoms,Result)

thus setting the initial value of Symptoms to [].

When Done is entered, the list is unified with Result and passed back upon back-chaining. Normally the variables would be named Symptoms0 and Symptoms but I kept them this way so they are easier to follow. Other wise there would be variables Symptom, Symptoms and Symptoms0, but once you get use to them are easier to follow.

The check for duplicates is done using memberchk/2 which is better than using member/2 for checking. Again this adds another conditional to the mix.

Example run:

?- getSymptoms(Symptoms).
Please enter symptoms now, enter "Done" when finished: headache
Please enter symptoms now, enter "Done" when finished: malaise
Please enter symptoms now, enter "Done" when finished: headache
Please enter symptoms now, enter "Done" when finished: Done
Symptoms = [malaise, headache].
Guy Coder
  • 24,501
  • 8
  • 71
  • 136
  • Wow thank you for putting all that time into this. A couple questions I had were that I was on track were you have this "valid_symptom(Symptom,Symptoms) :- ( symptom(_,Symptom).." but I was trying to keep which disease the symptom related to. Im trying something like this "checkSymptom(Symptom,Symptoms) :- ( symptom(Disease,Symptom)->getSymptoms(Symptoms0), Symptoms = [Symptom|Symptoms0], Diseases=[Disease|Diseas0]..." is this a way to do that? – user10876930 Feb 15 '19 at 21:39
  • 1
    @user10876930 I would not do it that way because you are creating two separate list e.g `Symptoms = [Symptom|Symptoms0]`, `Diseases=[Disease|Diseas0].`that require both to always be in sync. If an item is deleted from one you get problems. Better to create list with tuples, or even better compound terms, e.g. `[symptom(shingles,headache), symptom(shingles,itching)]`. I know this looks like you are just copying the values from the symptoms facts, but it will work better. You should ask that as a separate question and get more points. – Guy Coder Feb 15 '19 at 21:49
  • 1
    @user10876930 If you do ask that question, think about how you need the data for the next part and explain that in the question. If you ask the question the way you wrote it here, when you get several symptoms with several diseases the data can be arranged differently when be extracted and safe the trouble of rearranging it again, e.g. `[XYZ,[ABC,DEF],UVW,[GHI,JKL,MNO]]` where the first part is the disease and the second part is a list of symptoms. – Guy Coder Feb 15 '19 at 21:53
  • Ok I see what you're saying about having to keep the two lists in sync. I'm going to look into the tuple/compound lists now. Thank you again for the time you spent answering the question and providing explanation! – user10876930 Feb 16 '19 at 23:49