Enumerating a context-free language

Posted on 2008-05-02 by Luke Palmer.
Categories: Algorithms, Code, Haskell.

Here is a familiar context-free grammar for arithmetic expressions:

S      ::= add
add    ::= mul | add + mul
mul    ::= term | mul * term
term   ::= number | ( S )
number ::= digit | digit number
digit  ::= 0 | 1 | ... | 9

I have a challenge for you: write a program (in a language of your choice) which enumerates all strings accepted by this grammar. That is, your program runs forever, but if I have a string this grammar accepts, then your program should output it in a finite amount of time.

(This really is a fun challenge, so I encourage interested readers to try it themselves)

This is a tricky problem. Indeed it is sufficiently tricky that I cannot think of a clean imperative solution to it (which I’m sure is also related to being very out-of-practice in imperative programming). I’m interested to see any such solutions that people came up with. (And I’ll try it myself in the meantime)

But I’m going to present a functional solution using a neat little monad called Omega which I just uploaded to Hackage.

Let’s step back and consider a simpler motivating example. Consider the following list comprehension (and recall that list comprehensions are just the list monad behind a little bit of sugar):

pairs = [ (x,y) | x <- [0..], y <- [0..] ]

This looks like it generates all pairs of naturals, but it does not. It generates the list [(0,0), (0,1), (0,2), ...], so the first element of the pair will never get to 1. If Haskell allowed us to use ordinal numbers as indices then we could: pairs !! ω == (1,0). :-)

Conceptually what we have is a lattice of pairs that we’re “flattening” poorly:

(0,0)  (0,1)  (0,2)  (0,3)  ...
(1,0)  (1,1)  (1,2)  (1,3)  ...
(2,0)  (2,1)  (2,2)  (2,3)  ...
  .      .      .      .

We’re flattening it by taking the first row, then the second row, and so on. That’s what concat does. But anybody who’s had a brief introduction to set theory knows that that’s not how you enumerate lattices! You take the positive diagonals: (0,0), (1,0), (0,1), (2,0), (1,1), (0,2), .... That way you hit every element in a finite amount of time. This is the trick used to show that there are only countably many rational numbers.

Omega is the monad that comes from this concept. We define Omega’s join to be this “diagonal” function. What we get back is a monad with a nice property:

If x occurs at a finite position in xs and y occurs at a finite position in f x, then y occurs at a finite position in f =<< xs

It’s hard to know what that means without knowing what =<< is supposed to mean. It means if you have a (possibly infinite) list of items, and to each one you apply a function which generates another (possibly infinite) list, and you merge them all together, you’ll be able to reach every result in a finite time.

More intuitively, it means if you write a multiple branching recursive function where each branch recurses infinitely (generating values), you will not get stuck in the first branch, but rather generate values from all branches.

And that is exactly what we need to write the context-free enumerator. Given a simple data structure representing a context-free grammar:

data Symbol a
    = Terminal a
    | Nonterminal [[Symbol a]] -- a disjunction of juxtapositions

Then we can write an enumerate function in a straightforward depth-first way:

enumerate (Terminal a) = return [a]
enumerate (Nonterminal alts) = do
    alt <- each alts          -- for each alternative
      -- (each is the Omega constructor :: [a] -> Omega a)
    rep <- mapM enumerate alt -- enumerate each symbol in the sequence
    return $ concat rep       -- and concatenate the results

But the Omega monad will do some heavy lifting and stagger those generators for us. Defining the grammar above:

arithGrammar = s
    where
    s      = Nonterminal [[add]]
    add    = Nonterminal [[mul], [add, Terminal '+', mul]]
    mul    = Nonterminal [[term], [mul, Terminal '*', term]]
    term   = Nonterminal [[number], [Terminal '(', s, Terminal ')']]
    digit  = Nonterminal $ map (map Terminal . show) [0..9]
    number = Nonterminal [[digit], [digit, number]]

And then running our enumerator:

runOmega $ enumerate arithGrammar

We get a very encouraging list of results:

0
1
0+0
0*0
0+1
(0)
1+0
0*1
0+0*0
00
...

Notice how each type of node is represented in that initial prefix. That’s a good sign that there won’t be degenerate repetitive behavior. But of course we know there won’t be by the guarantee Omega makes (unless I implemented it wrong).

33 comments.

Tac-Tics
Comment on May 2nd, 2008.

Omega seems like a pretty cool monad. I remember running into this issue when I started using Haskell, and it frustrated me. This solution is just so wonderful.

Rafael
Comment on May 2nd, 2008.

One solution is to simply
generate a list of strings of length n
output every string on the list that can be parsed with the given grammar
although perhaps this can’t be called clean.

But my point is that I don’t quite understand what the enumerating pairs has to do with it. Since the alphabet is finite, you don’t have to use diagonalization.

Black Meph
Comment on May 2nd, 2008.

I’d like to see your Omega monad, since it sounds suspiciously like a ZipList/stream.

Tac-Tics
Comment on May 2nd, 2008.

@Rafael
I’m pretty sure that the above way is a much more efficient way of searching a context free language. With your method is entirely feasible, of course, but it requires parsing obviously nonsensical strings like “—” and “01-+3″. The above method never has to consider these, as they aren’t produced by the production rules.

The reason for using the diagonalization monad is that the naive approach to enumerating the production using the list monad is depth-first, and because the S-production rule here is unbounded (an expression can be as long as you want to make it), the tree is infinitely deep.

From what I’m understanding, omega is simply a breadth-first variation on the list monad.

Comment on May 2nd, 2008.

I would tend to agree with Tac-Tics, it should be quite simple as a depth first search. I could probably hack something together by this evening, but here would be my general approach (in C++):

1) Create a queue of objects representing strings (or lists) of terminals and non-terminals.
2) Push the Start symbol on the queue.
3) At each iteration, pop the queue. If it is all terminals, print it. Otherwise apply every possible production to it and push the results onto the queue.

Comment on May 2nd, 2008.

Ehrm, s/depth (first search)/breadth $1/

pb
Comment on May 2nd, 2008.

This is in pseudo-Python:

res := {S}
for s in res:
  for A -> X in productions:
    for each position p of S
        such that s[p] = A
      s' := s[p <- X]
      if s' not in res:
        res := res U s'
        if s' has no nonterminals:
          yield s'
Mike
Comment on May 2nd, 2008.

How about Goedel numbers in reverse?

Comment on May 3rd, 2008.

Black Meph: the Omega monad is literally just a list with “diagonal” instead of “concat” for join.

pb, yours and The Plaid Mentat’s solutions are quite similar. I guess the reason I didn’t see it is because of too “typed” a world view; putting nonterminals and terminals together in list didn’t occur to me… even though I did that to construct the grammar in the first place.

Mike: I know what a Gödel number is, but I have no idea what you’re talking about.

Mike
Comment on May 3rd, 2008.

Sorry Luke, I mean (i) write a function that maps natural numbers to expressions, then (ii) iterate through them. Perhaps less elegant in practice than I first thought, but it’s definitely possible.

Comment on May 3rd, 2008.

A function like (enumerate arithGrammar !!)? :-)

That’s kind of the whole point of Omega, to enumerate recursive things.

Mike
Comment on May 3rd, 2008.

Me again. It’s certain easy enough my way (a few dozen lines of my amateurish Ruby) to generate a promising-looking expression sequence, but to do it without duplicates is another matter entirely. Thanks, interesting problem, yours is by far the neater solution!

anonymous
Comment on May 3rd, 2008.

Have you tried to make your Omega monad an instance of MonadPlus? The obvious instances (modulo newtype wrapping: mzero = [], mplus = merge, with merge merging two lists) fail to work, consider for example:


do
x <- each [0..]
y <- each [0..]
guard $ y == 3
return (x,y)

Anon2
Comment on May 3rd, 2008.

I believe Omega falls under the rubric of fair backtracking a.k.a. interleaving monad transformers in the literature.

MonadPlus
Comment on May 3rd, 2008.

@Anonymous:

Trivial.

instance MonadPlus Omega where
mzero = Omega []
mplus (Omega x) (Omega y) = Omega $ diagonal [x,y]

anonymous
Comment on May 3rd, 2008.

@MonadPlus: The problem is not quite as trivial as it seems from your post: The definitions of mzero and mplus you’ve given are very obvious, of course.

But they fail to work properly! If you try my example, do { x <- each [0..]; y <- each [0..]; guard (y == 3); return (x,y) }, you’ll see that it goes infinite loop, instead of lazily returning the infinite list [(0,3),(1,3),(2,3),…].

MonadPlus
Comment on May 3rd, 2008.

You’re right! It’s not quite so trivial. On the other hand, this is entirely well-known territory, albeit arguably without a generic one-size-fits-all solution.

Hint: diagonal . (diagonal `fmap`) == diagonal . diagonal, except when it’s not.

The Oleg-Shan axis has a different approach, I believe.

Comment on May 3rd, 2008.

A solution in python (sorry for the ugly combine function).

http://pastebin.com/f4084533a

Comment on May 3rd, 2008.

p: Very nice solution in python. The use of generators really cleans things up.

anonymous: Indeed that sort of thing was exactly what I originally created this for (long ago). My solution was to define:

newtype Omega a = Omega [Maybe a]

With mzero defined as Omega [Nothing].

I found that quite ugly. It also was not very efficient. I have yet to find a better solution, though. The hackage module does not do this…

pb
Comment on May 3rd, 2008.

Another rather hackish implementation.

import re

def gen(P, start=''):
  t = {start: 1}
  ns = [start]
  while ns != []:
    n = ns.pop(0)
    for A, Xs in P.items():
      for X in Xs:
        for i in range(len(n)):
          for match in re.finditer('', n):
            m = n[:match.start()] + X + n[match.end():]
            if not t.has_key(m):
              t[m] = 1
              if '<' not in m:
                yield m
              else:
                ns.append(m)

prods = {
  'S': ['', ''],
  'Digit': '0123456789',
}

for x in gen(prods):
  print x
pb
Comment on May 3rd, 2008.

Sorry, should be

##
‘S’: [”, ”],

jav
Comment on May 5th, 2008.

I implemented Plaid Mentat’s solution in Haskell (see http://hpaste.org/7407), but I believe the strategy only works in theory. The reason: While it is a breadth first algorithm of sorts, it will quickly fill the queue with long combinations of non-terminals that produce even longer combinations and so on and you never reach a finished state in a reasonable time. The above program prints the digits 0 to 9 and then just runs with no output for quite some time.

Comment on May 5th, 2008.

Jav - I had that problem as well with my initial solution, but I found that if a little more care is taken about how you fill the queue, you can get around this problem.

If you apply all possible productions to the first non-terminal, then to the second non-terminal, etc, you get the queue explosion you’re talking about (as well as duplicates) because you leave the first one unchanged when you move to the second one. One solution I considered was to work recursively here, but I took the easy way out and simply applied all possible productions to the /first/ non-terminal and stopped. This made that step of the algorithm much faster, made the queue fill much more slowly, and worked pretty well overall (I got all ~10 character expressions in about 2-3 minutes).

You’ll excuse my jumping to conclusions if this isn’t what yours does, as I can’t read haskell quite well yet - but I hope that helps!

jav
Comment on May 6th, 2008.

Plaid Mentat, thx for your comment. I came to a similar conclusion, even though I’m pretty confident that I don’t have any duplicates. Whenever I touch an element of the queue I in fact apply productions to all non-terminals but I generate all possible combinations of all possible productions right away and then never touch that element again. I check the first 10000 elements and don’t have duplicates there. I tried to apply your strategy of only working on the first non-terminal but it wouldn’t work for me.

jav
Comment on May 6th, 2008.

In any case. I tried another solution. This one is inspired by the Python solution posted earlier as that program was pretty functional already. It uses the idea of alternating between productions which traverses the tree of possible derivations in a much more spread-out manner.

Initial code: http://hpaste.org/7432
Made more compact: http://hpaste.org/7433

jav
Comment on May 6th, 2008.

And the idea further generalized so that it should work with any grammar: http://hpaste.org/7434

As it turns out the last code example is actually pretty similar to Luke’s solutions . But instead of using the Omega Monad it uses the ‘alternate’ function.

Comment on May 6th, 2008.

jav: hehe, it was fun watching you get closer and closer to the Omega solution.

And as you noticed, I think if you replaced the occurrences of alternate with diagonal from Control.Monad.Omega, you get exactly my solution. You could implement a BreadthFirst monad or something using alternate in the same way. Hmm… I wonder if I can factor diagonal out of that monad an parameterize it on the join function (the answer is yes).

After all this, here’s a summary of using list combining functions as traversals:

  • concat - depth first
  • alternate - breadth first
  • diagonal - diagonal?

The reason the diagonal traversal is important is that it still traverses the whole tree even if it has an infinitely deep path or infinitely wide branch (where depth- and breadth-first traversals fail, resp.).

MonadPlus
Comment on May 6th, 2008.

A riddle:

concat = concat . id
alternate = concat . transpose
diagonal = concat . ???

Comment on May 6th, 2008.

MonadPlus: stripe (approximately as used in diagonal’s implementation).

stripe [[1, 2, 3]
       ,[4, 5, 6]
       ,[7, 8, 9]]
= [[1],[4,2],[7,5,3],[8,6],[9]]

Another answer is (:[]) . diagonal :-)

MonadPlus
Comment on May 7th, 2008.

The Force is with you, young Palmer.
But you are not a Jedi yet.

There is more than one stripe function, if you consider renaming the one you gave as rotate45.

What does this tell you, if anything, about the free theorem(s) associated with the type [[a]] -> [[a]]?

apfelmus
Comment on May 8th, 2008.

Hello Luke,

good work, but I have the suspicion that Omega doesn’t fulfill the monad laws, at least not in the sense that the lists are equal, only in the sense that they have the same elements (as sets). In other words, I think that

  join . join == join . fmap join

with join = diagonal doesn’t hold with list equality, it only works with set equality.

Comment on May 9th, 2008.

Daft, you’re right! As an introduction to Isabelle I was going to attempt to prove the monad laws for Omega, but I was too lazy.

Nor does the alternate function we were discussing, for that matter.

I wonder, is concat the only function :: [[a]] -> [a] that is a proper join?

I suspect that the effect of the monad laws not holding on Omega is that (>=>) is not associative. That seems like a major problem. Time to add a disclaimer to the hackage module.

apfelmus
Comment on May 13th, 2008.

I’m not sure whether concat is the only proper join, but at least I can say that there is no proper join for infinite lists.

Proof: Every join for infinite lists / diagonalization corresponds to a bijective function \circ:\mathbb{N}\times\mathbb{N}\to\mathbb{N}.
Now, the third monad law corresponds to the associativity of this binary operation \circ, i.e. the equation a \circ (b \circ c) = (a \circ b) \circ c. But since it’s supposed to be bijective, we have a = a \circ b, a contradiction.

(PS: Can you make this undersized comment box bigger? And a preview would be cook, too :-) )

Leave a comment

Names and email addresses are required (email addresses aren't displayed), url's are optional.

Comments may contain the following xhtml tags:
<a href="" title=""> <abbr title=""> <b> <blockquote cite=""> <code> <i> <pre> <q cite=""> <strike> <ol> <ul> <li>

Embedded LaTeX math mode allowed using the syntax: $latex e^{i \pi} + 1 = 0$.






Prove you're human. Type "human".