简体   繁体   中英

Implementing an Automaton in Prolog

I'm new to Prolog. I managed to learn C and Java relatively quickly but and Prolog is giving me a lot of trouble. My trouble is understanding lists and writing functions? For example. We have this automaton:

在此处输入图像描述

I can do this task in C and Java, no problems. But the course wants Prolog. With my current knowledge I could do things like this:

% 1. Check whether all integers of the list are < 10.
less_than_10([]).
less_than_10([Head|Tail]) :-
    Head < 10,
    less_than_10(Tail).

Just so you know where my knowledge is at. Very basic. I did read the list chapter in Learn Prolog Now but it's still confusing me. They gave us a hint:

Every node should be presented like:

delta(1, d, 2)
% or
alpha(2, a, 2)

They also told us to pass the list in questions to a predicate that returns true if the list fits the automaton and false if not:

accept([d,a,b,a,b,b,b,c,d,c]).

The output is true .

Where to go from here? I'm guessing the first step is to check if the Head of the list is 1 . How do I do that? Also, should I add every node as fact into the knowledge base?

So that's pretty easy. Super-direct, much more than if you were using C or Java.

Let's write an interpreter for this graph that:

  • Is given a list of named transitions ;
  • Walks the transitions using the given graph along a path through that graph ;
  • Accepts (Succeeds) the list if we end up at a final state ;
  • Rejects (Fails) the list if we do not ;
  • And.. let's say throws an exception if the list cannot be generated by the given graph.

Prolog gives us nondeterminism for free in case there are several paths. Which is nice.

We do not have an class to describe the automaton. In a sense, the Prolog program is the automaton. We just have a set of predicates which describe the automaton via inductive definitions. Actually, if you slap a module definition around the source below, you do have the object.

First describe the graph. This is just a set of Prolog facts.

As required, we give the transitions (labeled by atoms) between nodes (labeled by integers), plus we indicate which are the start and end nodes. There is no need to list the nodes or edges themselves.

delta(1,d,2).
delta(2,a,2).
delta(2,b,2).
delta(2,d,4).
delta(2,e,5).
delta(2,c,3).
delta(3,d,6).
delta(6,c,5).
start(1).
end(4).
end(5).

A simple database. This is just one possible representation of course.

And now for the graph walker. We could use Definite Clause Grammars here because we are handling a list, but lets' not.

First, a predicate which "accepts" or "rejects" a list of transitions.

It looks like:

% accepts(+Transitions)

It starts in a start state , then "walks" by removing transitions off the list until the list is empty. Then it checks whether it is at an end state .

accepts(Ts) :-               % accept the list of transitions if... 
   start(S),                 % you can accept the list starting 
   accepts_from(S,Ts).       % from a start state  
                       

accepts_from(S,[T|Ts]) :-    % accepts the transitions when at S if...
   delta(S,T,NextS),         % there is a transition S->NextS via T
   accepts_from(NextS,Ts).   % and you can accept the remaining Ts from NextS. (inductive definition)

accepts_from(S,[]) :-        % if there is no transition left, we accept if...
   end(S).                   % we are a final state

Ah, we wanted to throw if the path was impossible for that graph. So a little modification:

accepts(Ts) :-               % accept the list of transitions if... 
   start(S),                 % you can accept the list starting 
   accepts_from(S,Ts).       % from a start state  
                       

accepts_from(S,[T|Ts]) :-    % accepts the transitions when at S if...
   delta(S,T,NextS),         % there is a transition S->NextS via T
   accepts_from(NextS,Ts).   % and you can accept the remaining Ts from NextS.

accepts_from(S,[T|Ts]) :-    % accepts the transitions when at S if...
   \+ delta(S,T,NextS),      % there is NO transition S->NextS via T
   format(string(Txt),"No transition at ~q to reach ~q",[S,[T|Ts]]),
   throw(Txt).

accepts_from(S,[]) :-        % if there is no transition left, we accept if...
   end(S).                   % we are a final state

And so:

?- accepts([d,a,b,a,b,b,b,c,d,c]).
true ;   % yup, accepts but maybe there are other paths?
false.   % nope

?- accepts([d,a,a,a,a,e]).
true ;
false.

?- accepts([d,a,a,a,a]).
false.

?- accepts([d,c,e,a]).
ERROR: Unhandled exception: "No transition at 3 to reach [e,a]"

The above code should also be able to find acceptable paths through the graph. But it does not:

?- accepts(T).
... infinite loop

This is not nice.

The primary reason for that is that accept/2 will immediately generate an infinite path looping at state 2 via transitions a and b . So one needs to add a "depth limiter" (the keyword is "iterative deepening").

The second reason would be that the test \+ delta(S,T,NextS) would succeed at node 4 for example (because there is nowhere to go from that node) and cause an exception before trying out the possibility of going nowhere (the last clause). So when generating, throwing is a hindrance, one just wants to reject.

Addendum: Also generate

The following only accepts/rejects and does not throw, but can also generate.

:- use_module(library(clpfd)).

accepts(Ts,L) :-                 % Accept the list of transitions Ts of length L if
   start(S),                     % ...starting from a start state S
   accepts_from(S,Ts,L).         % ...you can accept the Ts of length L.                       

accepts_from(S,[T|Ts],L) :-      % Accept the transitions [T|Ts] when at S if
   (nonvar(L)
    -> L >= 1
    ;  true),                    % L (if it is bound) is at least 1 (this can be replaced by L #> 0)
   delta(S,T,SN),                % ...and there is a transition S->SN via T
   Lm #= L-1,                    % ...and the new length is **constrained to be** 1 less than the previous length
   accepts_from(SN,Ts,Lm).       % ...and you can accept the remaining Ts of length Lm from SN.

accepts_from(S,[],0) :-          % If there is no transition left, length L must be 0 and we accept if
   end(S).                       % ...we are a final state.

delta(1,d,2).
delta(2,a,2).
delta(2,b,2).
delta(2,d,4).
delta(2,e,5).
delta(2,c,3).
delta(3,d,6).
delta(6,c,5).
start(1).
end(4).
end(5).

generate :-
   between(0,7,L),
   findall(Ts,accepts(Ts,L),Bag),
   length(Bag,BagLength),
   format("Found ~d paths of length ~d through the graph\n",[BagLength,L]),
   maplist({L}/[Ts]>>format("~d : ~q\n",[L,Ts]),Bag).

And so:

?- accepts([d,a,b,a,b,b,b,c,d,c],_).
true ;
false.

?- accepts([d,a,a,a,a],_).
false.

?- accepts([d,c,e,a],_).
false.

?- generate.
Found 0 paths of length 0 through the graph
true ;
Found 0 paths of length 1 through the graph
true ;
Found 2 paths of length 2 through the graph
2 : [d,d]
2 : [d,e]
true ;
Found 4 paths of length 3 through the graph
3 : [d,a,d]
3 : [d,a,e]
3 : [d,b,d]
3 : [d,b,e]
true ;
Found 9 paths of length 4 through the graph
4 : [d,a,a,d]
4 : [d,a,a,e]
4 : [d,a,b,d]
4 : [d,a,b,e]
4 : [d,b,a,d]
4 : [d,b,a,e]
4 : [d,b,b,d]
4 : [d,b,b,e]
4 : [d,c,d,c]
true 

Here's my answer. I sought to completely separate the data from the logic. There are rules to infer the possible paths, start and end nodes.

The edge/2 predicate stands for either an alpha or a delta line. The path (DCG) predicate describes a list of edges that ends with an end node. The start and end nodes are inferred using the start_node/1 and end_node/1 predicates. Finally, the phrase/3 is used to describe the list of paths that are valid automata.

delta(1, d, 2).
delta(2, d, 4).
delta(2, e, 5).
delta(2, c, 3).
delta(3, d, 6).
delta(6, c, 5).

alpha(2, a, 2).
alpha(2, b, 2).


edge(Node, Node, Via) :-
    alpha(Node, Via, Node).

edge(From, To, Via) :-
    delta(From, Via, To).


path(From, To) -->
    { end_node(To),
      dif(From, To),
      edge(From, To, Via)
    },
    [Via].

path(From, To) -->
    {edge(From, Mid, Via)},
    [Via],
    path(Mid, To).


start_node(Node) :-
    node_aux(start_node_aux, Node).

end_node(Node) :-
    node_aux(end_node_aux, Node).

start_node_aux(Node) :-
    edge(Node, _, _),
    \+ edge(_, Node, _).

node_aux(Goal, Node) :-
    setof(Node, call(Goal, Node), Nodes),
    member(Node, Nodes).

end_node_aux(Node) :-
    edge(_, Node, _),
    \+ edge(Node, _, _).


automaton -->
    {start_node(Start)},
    path(Start, _End).

accept(Steps) :-
    length(Steps, _N),
    phrase(automaton, Steps).

I suspect that David did not use Definite Clause Grammars because you should be familiar with the basics before learning DCGs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM