PrologEZ
Advanced · Lesson 33 of 43

Grammars with DCGs

Parse and generate language with Definite Clause Grammars.

Prolog was born for natural-language processing, and it has built-in syntax for it: Definite Clause Grammars. A grammar rule uses --> instead of :-:

greeting --> [hello], name.

Read it as "a greeting is the word hello followed by a name". Terminals, actual words, go in square brackets; non-terminals are other rules.

greeting --> [hello], name.
name --> [world].
name --> [prolog].

Run a grammar with \phrase(Rule, List)\: does the list match the rule?

?- phrase(greeting, [hello, world])
?- phrase(greeting, [hello, there])

Like everything in Prolog it runs backwards too, leave the list open and Prolog generates sentences:

?- phrase(greeting, Words)
?- phrase(greeting, [hello, Who])

What --> really is

A DCG rule is syntactic sugar. Each non-terminal gets two extra arguments, a list of words before and after, and each rule describes how it consumes words from the front of the list. listing shows the translation:

?- listing(greeting/2), listing(name/2)

Because of the two hidden arguments you can even call the rule directly: greeting([hello, world], []) means "greeting consumes [hello, world], leaving nothing".

A bigger grammar

sentence    --> noun_phrase, verb_phrase.
noun_phrase --> determiner, noun.
verb_phrase --> verb, noun_phrase.

determiner --> [the] ; [a].
noun       --> [cat] ; [dog] ; [robot].
verb       --> [sees] ; [chases].
?- phrase(sentence, [the, cat, chases, a, robot])
?- phrase(sentence, [cat, the, chases, a, robot])
?- aggregate_all(count, phrase(sentence, _), N)

That grammar accepts exactly 72 sentences (2 determiners × 3 nouns × 2 verbs × 2 × 3).

Building a parse tree: extra arguments

Non-terminals can carry arguments, so a parser can return structure:

s(s(NP, VP))     --> np(NP), vp(VP).
np(np(D, N))     --> det(D), n(N).
vp(vp(V, NP))    --> v(V), np(NP).

det(det(the)) --> [the].
det(det(a))   --> [a].
n(n(cat))     --> [cat].
n(n(dog))     --> [dog].
v(v(sees))    --> [sees].
v(v(chases))  --> [chases].
?- phrase(s(Tree), [the, cat, sees, a, dog])

Curly braces: ordinary Prolog inside a rule

{ Goal } runs plain Prolog without consuming input. That's how you do arithmetic or type checks, here, reading numbers out of text. Text is a list of character codes; backquotes make such a list: \123\``.

digits([D|T]) --> digit(D), digits(T).
digits([D])   --> digit(D).
digit(D)      --> [D], { code_type(D, digit) }.

number(N) --> digits(Ds), { number_codes(N, Ds) }.
?- phrase(number(N), `1234`)
?- atom_codes('2024', Codes), phrase(number(N), Codes)

A calculator in 15 lines

Put it together: a grammar for +, -, *, and parentheses that also evaluates as it parses. Note the structure, * binds tighter than + because it lives lower in the grammar, and the use of loops (..._rest) rather than left recursion, which would never terminate.

expr(V)  --> term(T), expr_rest(T, V).
expr_rest(Acc, V) --> "+", term(T), { Acc1 is Acc + T }, expr_rest(Acc1, V).
expr_rest(Acc, V) --> "-", term(T), { Acc1 is Acc - T }, expr_rest(Acc1, V).
expr_rest(V, V)   --> [].

term(V)  --> factor(F), term_rest(F, V).
term_rest(Acc, V) --> "*", factor(F), { Acc1 is Acc * F }, term_rest(Acc1, V).
term_rest(V, V)   --> [].

factor(V) --> number(V).
factor(V) --> "(", expr(V), ")".

calc(Text, Value) :- string_codes(Text, Codes), phrase(expr(Value), Codes).
?- calc("2+3*4", V)
?- calc("(2+3)*4", V)
?- calc("10-4-3", V)
?- calc("2+", V)

Exercise: The language aⁿbⁿ

Write a grammar rule anbn that accepts lists made of some number of as followed by the same number of bs: [], [a,b], [a,a,b,b], … but not [a,b,b] or [b,a].