Learning Prolog: Warm up for semester 2 with the cinema
Learning Prolog is back, because apparently I am totally nuts and like to write long blog posts about the hardest programming language to use in existence. To kick off semester 2, we were given an old challenge: Write a cinema program that decides whether somebody can watch a given film or not. I remember this from my very first programming lab with Rob Miles - the first ever task that we were given was to build this exact program in C♯. Because Prolog is such a nasty and difficult language to learn, it has taken until now to learn all the concepts that you would need to write such a program.
In this post, I hope to guide you through the process of writing such a program in Prolog. To start with, here's an overview of what we will be building:
A Prolog program that will state whether or not a customer can see a movie depending on their age, the movie they want to see, and whether they have an adult with them.
We will need a few films to test this with. Here are the films that I was given in the lab:
Film Name |
Rating |
How to Lose Friends and Alienate People |
15 |
Death Race |
15 |
Space Chimps |
U |
The Chaser |
18 |
Get Smart |
12A |
The first job here is to convert the above table into Prolog:
film('How to Lose Friends and Alienate People', 15).
film('Death Race', 15).
film('Space Chimps', 'U').
film('The Chaser', 18).
film('Get Smart', '12A').
Now we need some way to output the above so that the customer can see which films we are currently showing. Here's what I came up with:
all_films(FilmList) :-
findall([ Film, Rating ], film(Film, Rating), FilmList).
list_films :-
all_films(FilmList),
list_films(FilmList, 0).
list_films([], _).
list_films([ [ FilmName, FilmRating ] | Rest ], Counter) :-
write(Counter), write(' - '), write(FilmName), write(' - '), write(FilmRating), nl,
NCounter is Counter + 1,
list_films(Rest, NCounter).
As you should have come to expect with Prolog, the solution here is recursive. Firstly I define a rule to fetch me all the films with a quick findall/3
. Then I define list_films/1
which calls all_films/1
to get all the films and passes them over to list_films/2
, which actually does the work. I also have a counter here to keep track of the index of each film. This is important because we want to get the user to enter the number of the film that they want to watch.
Here's some example output of the above:
?- list_films.
0 - How to Lose Friends and Alienate People - 15
1 - Death Race - 15
2 - Space Chimps - U
3 - The Chaser - 18
4 - Get Smart - 12A
true.
This works by fetching all the films and loading them each into a list that contains their name and rating. These lists are then packaged into one big list, which is then spun through and outputted one by one, whilst incrementing the counter variable.
The next part of this problem is asking the user which film they want to see. Surprisingly, this isn't too tough.
ask_film(FilmNumber) :-
write('Enter the number of the film that you would like to see: '),
read(FilmNumber),
true.
cinema_loop :-
repeat,
write('Welcome to our multiplex.'), nl,
write('We are presently showing:'), nl,
list_films,
ask_film(FilmNumber),
write('You chose option '), write(FilmNumber), write('.').
Here I define another rule to ask the user which film they want to see, and then define the main cinema loop. The main loop displays a welcome message, lists all the films that we are presently showing using the code we devised above, asks the user which film they want ot watch, and tells them which number they selected.
We are getting there, but telling the user which option they selected is a bit pointless, if you ask me. Let's upgrade it to tell us which film we selected and it's rating.
film_rating(FilmNumber, FilmRating) :-
all_films(FilmList),
nthelement(FilmNumber, FilmList, [ FilmName, FilmRating ]),
write('You want to see '), write(FilmName), write(', which is a '), write(FilmRating), nl.
cinema_loop :-
repeat,
write('Welcome to our multiplex.'), nl,
write('We are presently showing:'), nl,
list_films,
ask_film(FilmNumber),
film_rating(FilmNumber, FilmRating),
check_age(FilmRating),
write('Enjoy the film!'), nl, true.
My code starts to get a little bit messy here (sorry!), but I've taken the cinema_loop/1
that we started above and upgraded to call film_rating/2
. This new rule actually has a rather misleading name now that I think about it, but it's purpose essentially is to take a given film number and to return it's rating, whilst telling the user which film they selected.
film_rating/2
calls another rule that you might find familiar - the nth_element
rule that I wrote in semester 1's last lab. GO and take a look at my other post if you are confused.
Now that we have that connector in place, we can start thinking about ratings. The rating system isn't as simple as it sounds when you start to think about it:
Rating |
Meaning |
U |
Universal, suitable for all ages |
PG |
Parental Guidance, suitable for all ages at parents discretion |
12 |
No one younger than 12 can see this film |
12A |
No one younger than 12 can see this film unless accompanied by an adult |
15 |
No one younger than 15 can see this film |
18 |
No one younger than 18 can see this film |
Let's brack this down into chunks. In theory, we can do this with a single rule with 2 arities one for those rating that don't require the age of the customer, and those that do. We can write one part of the rule for each rating. Let's start with U
:
check_age(FilmRating) :-
FilmRating = 'U'.
Nice and simple, right? It succeeds if the film rating is 'U'. Let's move on. We can lump PG
and 12A
together if the customer is under 12 years of age:
check_age(FilmRating, Age) :-
member(FilmRating, [ 'PG', '12A' ]),
Age < 12,
ask_adult(Adult),
Adult = yes.
The above simple makes sure that the rating is either 'PG' or '12A', then it make sure that the customer is under 12, then it makes sure that the customer has an adult with them usng the rule below. If all of these comditions are met, then it succeeds.
ask_adult(Adult) :-
write('Are you accompanied by an adult?'),
read(Answer),
member(Answer, [y, yes, yep]),
Adult = yes,
true.
If the user is 12 or over and wants to see a PG or a 12A, then we should let them through without asking them about an adult.
check_age(FilmRating, Age) :-
member(FilmRating, [ 'PG', '12A' ]),
Age >= 12.
Next we need to deal with the regular 12, 15, and 18 ratings. There are easy:
check_age(FilmRating, Age) :-
not(FilmRating = 'PG'), not(FilmRating = '12A'),
Age >= FilmRating.
We then need to connect the check_age/1
with the check_age/2
. This is also fairly simple:
ask_age(Age) :-
write('What is your age? '),
read(Age).
check_age(FilmRating) :-
ask_age(Age),
check_age(FilmRating, Age).
If all else fails, then the customer mustn't be allowed to see the film. We should add another quick rule to tell the customer that they can't see the film before we are done:
check_age(_, _) :-
write('Sorry, you can\'t see that film - try picking another one.'), nl,
fail.
That completes the cinema program. Below you can find the source code for the whole thing that I based this post on. There are some unnecessary true.
s floating around, but they are just relics from when I was debugging it and couldn't figure out what the problem was.
% Current films
film('How to Lose Friends and Alienate People', 15).
film('Death Race', 15).
film('Space Chimps', 'U').
film('The Chaser', 18).
film('Get Smart', '12A').
% Main loop
cinema_loop :-
repeat,
write('Welcome to our multiplex.'), nl,
write('We are presently showing:'), nl,
list_films,
ask_film(FilmNumber),
film_rating(FilmNumber, FilmRating),
check_age(FilmRating),
write('Enjoy the film!'), nl, true.
% Get all films
all_films(FilmList) :-
findall([ Film, Rating ], film(Film, Rating), FilmList).
% Get the film with a given number
film_rating(FilmNumber, FilmRating) :-
all_films(FilmList),
nthelement(FilmNumber, FilmList, [ FilmName, FilmRating ]),
write('You want to see '), write(FilmName), write(', which is a '), write(FilmRating), nl.
% Get the nth element
% From https://starbeamrainbowlabs.com/blog/article.php?article=posts%2F134-learning-prolog-lab-10.html
nthelement(0, [ Head | _ ], Head).
nthelement(N, [ _ | Tail ], Result) :-
N > 0,
NRecurse is N - 1,
nthelement(NRecurse, Tail, Result).
% Check the user's age against the given film.
check_age(FilmRating) :-
FilmRating = 'U'.
check_age(FilmRating) :-
ask_age(Age),
check_age(FilmRating, Age).
check_age(FilmRating, Age) :-
member(FilmRating, [ 'PG', '12A' ]),
Age < 12,
ask_adult(Adult),
Adult = yes, true.
check_age(FilmRating, Age) :-
member(FilmRating, [ 'PG', '12A' ]),
Age >= 12.
check_age(FilmRating, Age) :-
not(FilmRating = 'PG'), not(FilmRating = '12A'),
Age >= FilmRating.
check_age(_, _) :-
write('Sorry, you can\'t see that film - try picking another one.'), nl,
fail.
% Gets the user's film preference
ask_film(FilmNumber) :-
write('Enter the number of the film that you would like to see: '),
read(FilmNumber),
true.
% Gets the user's age
ask_age(Age) :-
write('What is your age? '),
read(Age), true.
ask_adult(Adult) :-
write('Are you accompanied by an adult?'),
read(Answer),
member(Answer, [y, yes, yep]),
Adult = yes,
true.
% Lists all films
list_films :-
all_films(FilmList),
list_films(FilmList, 0).
list_films([], _).
list_films([ [ FilmName, FilmRating ] | Rest ], Counter) :-
write(Counter), write(' - '), write(FilmName), write(' - '), write(FilmRating), nl,
NCounter is Counter + 1,
list_films(Rest, NCounter).