2.2 Finite State Transducers

We will now introduce finite state transducers (or FSTs); another finite state machine that allows to produce output recording the structure of the input.

2.2.1 What are Finite State Transducers?

A finite state transducer essentially is a finite state automaton that works on two (or more) tapes. The most common way to think about transducers is as a kind of ``translating machine''. They read from one of the tapes and write onto the other. This, for instance, is a transducer that translates as into bs:

a:b at the arc means that in this transition the transducer reads a from the first tape and writes b onto the second.

Transducers can, however, be used in other modes than the translation mode as well: in the generation mode transducers write on both tapes and in the recognition mode they read from both tapes. Furthermore, the direction of translation can be turned around: i.e. a:b can not only be read as ``read a from the first tape and write b onto the second tape'', but also as ``read b from the second tape and write a onto the first tape''.

So, the above transducer behaves as follows in the different modes.

Transitions in transducers can make jumps going from one state to another without doing anything on either one or on both of the tapes. So, transitions of the form a:# or #:a or #:# are possible. Here is an example:

And what does this transducer do?

Similar as with FSAs, we can also use categories to label the arcs and provide a kind of lexicon which translates these categories into real labels, i.e. labels of the form X:Y. Here is an example translating English number terms into numbers.

And here is the lexicon that maps the category labels to standard FST transition labels:

lex(one:1,`ONES').
lex(two:2,`ONES').
lex(three:3,`ONES').
lex(four:4,`ONES').
lex(five:5,`ONES').
lex(six:6,`ONES').
lex(seven:7,`ONES').
lex(eight:8,`ONES').
lex(nine:9,`ONES').
lex(eleven:11,`TEENS').
lex(twelve:12,`TEENS').
...
lex(twenty:20,`TENS').
...
lex(zero:0,`ZERO').

2.2.2 FSTs in Prolog

In implementing finite state transducers in Prolog, we will follow the same strategy that we used for FSAs: we represent an FST as a static data structure that other programs manipulate.

Here is how we represent our first transducer, the a to b translator.

:- op(250,xfx,:).
 
initial(1).
final(1).
arc(1,1,a:b).

To be able to write a:b as the label of the arc, we have to define : as an infix operator as is done by the operator definition.

Our second transducer, the a doubler, looks like this in Prolog representation:

:- op(250,xfx,:).
 
initial(1).
final(1).
arc(1,2,a:a).
arc(2,1,#:a).

Now, we need a program that can manipulate these data structures and carry out the transduction. We will first assume that there are no jump arcs and extend recognize1 from the last chapter to work as a transducer.

The base case is that both tapes are empty and the FSA is in a final state. In Prolog:

transduce1(Node,[],[]) :-
    final(Node).

In the recursive case, we make a transition from one node to another which is licensed by some arc definition in the database. As in the last chapter we define a predicate traverse1 to check that the transition is indeed licensed.

transduce1(Node1,Tape1,Tape2) :-
    arc(Node1,Node2,Label),
    traverse1(Label,Tape1,NewTape1,Tape2,NewTape2),
    transduce1(Node2,NewTape1,NewTape2).
 
traverse1(L1:L2,[L1|RestTape1],RestTape1,[L2|RestTape2],RestTape2).

Finally, we define the following driver predicate testtrans1. It can be called with both arguements instantiated, only one of them instantiated, or both uninstantiated depending on which mode we want to use the transducer in.

testtrans1(Tape1,Tape2) :-
    initial(Node),
    transduce1(Node,Tape1,Tape2).

We can use this program to transduce as to bs with our first transducer. To be able to use the second transducer, the a doubler, as well, we need a program that can handle transitions involving jumps. What do we have to change for that? Well, the only thing that changes is the way that the tapes are treated when making a transition. This is taken care of by the traverse1 predicate and this is all we have to adapt. (Remember that when extending the recognizer of the last chapter to handle jump arcs, we also only changed the traverse1 predicate.)

So, what are the possibilites how a tape can be affected by a transition? There are four:

The Prolog definition of traverse2 therefore has four clauses:

traverse2('#':'#',Tape1,Tape1,Tape2,Tape2).
traverse2('#':L2,Tape1,Tape1,[L2|RestTape2],RestTape2).
traverse2(L1:'#',[L1|RestTape1],RestTape1,Tape2,Tape2).
traverse2(L1:L2,[L1|RestTape1],RestTape1,[L2|RestTape2],RestTape2).


Patrick Blackburn and Kristina Striegnitz
Version 1.2.4 (20020829)