Click here to Skip to main content
15,883,917 members
Articles / Programming Languages / Python

Creating a Turing Machine in Rust

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
30 Nov 2018CPOL10 min read 6.3K   4  
In this series, I am going to rewrite the Turing machine I wrote in Creating a Turing machine in Python.

Introduction

In this series, I am going to rewrite the Turing machine I wrote in Creating a Turing machine in Python. I am re-writing this to become more familiar with the Rust programming language and to improve my proficiency with it.

You can find the entire project on https://github.com/phillikus/rust_tm.

Let’s first revisit the fundamental theory behind Turing machines and set up the project based on that.

Requirements

  • Basic knowledge of the Rust programming language
  • Fundamentals of theoretical computer science

What Is a Turing Machine?

A Turing machine is a mathematical model of computation that reads and writes symbols of a tape based on a table rules. Each Turing machine can be defined by a list of states and a list of transitions. Based on a start state (s0), the Turing machine works its way through all the states until it reaches a final state (sf). If no transition leads to the final state, the Turing machine will run ‘forever’ and eventually run into errors.

A transition is defined by the current state, the read symbol at the current position on the tape, the next state and the next symbol that must be written to the tape. Additionally, it contains a direction to determine whether the head of the tape should move the left, right or not at all. To visualize this process, let’s take a look at a very simple Turing machine that increments the value from the initial tape by one.

Unary Increment

To keep it simple, we will start with a unary Turing machine. A one is represented by a ‘|’. So, a Turing machine that contains the number 3 would look like this:

$|||#

The $ represents the starting point of our Turing machine and # represents the end. After the Increment is complete, we expect the final tape to look like this:

$||||#

To reach that final state, our Turing machine simply has to read through all the ‘|’ and append a single ‘|’.

This can be represented by three states s0, s1 and sf with the following transitions:

(s0, ‘$’, s1, ‘$’, Right)
(s1, ‘|’, s1, ‘|’, Right)
(s1, ‘#’, sf, ‘|’, Neutral)

which can also be represented with the following state transition diagram:

 

Image 1

We start in state s0 where we expect to read a ‘$’. Then we switch to state s1 and write back that ‘$’. Then, we move the head one character to the right.

In state s1, as long as we read a ‘|’, we stay in state s1 and write back that ‘|’. This way, we will move all the way to the right end of the tape.

Finally, when we encounter an “#”, we write a ‘|’ and go to the final state sf. Since we are done, there is no reason to move the head at this point.

Project Structure

While this is a very simple Turing machine, we can use the same model to create machines of any complexity. With that knowledge, we are now ready to lay out the basic structure of our project. To represent the entire Turing machine, we will need one struct for the states, the transitions and the tape.

Additionally, we need an enumeration to represent the three directions in which we can move the head of the Turing machine: Left, Right and None (no movement).

Finally, we need one component that combines all these components and uses them to process the Turing machine. To set up the project, open a command line and type:

Bash
cargo new rust_tm

This should create a Cargo.toml file as well as a src/ directory with a lib.rs file. Deleting this file and adding a file for each of the structs we will need, I ended up with the following structure:

Image 2

We are now ready to dive right into the code, so let’s go!

direction.rs

This file contains the enum that will be used to determine in which direction the head of the tape should move. Its implementation is very straightforward:

Rust
#[derive(Copy)]
pub enum Direction {
    Right,
    Left,
    None
}

impl Clone for Direction {
    fn clone(&self) -> Direction { *self }
}

As you can see, I added an implementation for the Clone trait. You will see later why that is needed.

tape.rs

We will use this enum in the tape struct, defined in tape.rs:

Rust
use direction;

pub struct Tape {
    pub alphabet: Vec<char>,
    pub head_position: i32,
    pub tape: Vec<char>
}

As you can see, a tape consists of an alphabet which consists of all valid characters of the Turing Machine, its head position to know where it should read/write the next character and the tape itself, which is just a list of characters.

Since we can’t define methods in the struct itself, we will add those in its implementation:

Rust
impl Tape {
    pub fn new(alphabet: &str, tape: &str) -> Tape {
        Tape { alphabet: alphabet.chars().collect(), head_position: 0, tape: tape.chars().collect() }
    }

    pub fn write(&mut self, character: char) {
        if !(self.head_position >= 1 && self.alphabet.contains(&character)) {
            return
        }

        self.tape[self.head_position as usize] = character;
    }

    pub fn read(&mut self) -> char {
        if self.head_position as usize > self.tape.len() {
            panic!("Trying to read character at invalid position: {}.", 
                    self.head_position.to_string());
        }

        self.tape[self.head_position as usize]
    }

    pub fn move_head(&mut self, direction: direction::Direction) {
        match direction {
            direction::Direction::Right => { self.head_position += 1; },
            direction::Direction::Left => { self.head_position -= 1; },
            direction::Direction::None => {}
        }
        
        if self.head_position < 0 {
            self.head_position = 0;
        }

        if self.head_position >= self.tape.len() as i32 {
            self.tape.push('#');
        }
    }

    pub fn to_string(&self) -> String {
        self.tape.iter().collect()
    }
}

Let’s walk through it step by step:

Since Rust itself doesn’t provide any form of constructors, we will initialize the Tape in the new method, which takes a word and an alphabet as parameters. To make calling this method straightforward, both parameters take a &str which is then converted into a Vec<char>.

Additionally, we initialize the head_position to 0, representing the start of the tape.

The write and read methods simply read or write a character from/to the tape. If we try to write at position 0 (the start of the tape), or we try to write an invalid character, the code will simply return to prevent corrupt states.

When we try to read past the length of the tape, our code will panic and display an exception message.

The move_head method takes in a Direction and moves the head of the tape in that direction. Again, I added some bounds checks to prevent the head from exiting the bounds of the tape.

state.rs

This component contains another enum, which represents the type of the state as well as the State struct + implementation themselves:

Rust
#[derive(PartialEq)]
#[derive(Copy)]
pub enum StateType {
    Start,
    Empty,
    Final
}

#[derive(Copy)]
pub struct State {
    pub id: char,
    pub state_type: StateType
}

impl State {
    pub fn new(id: char, state_type: StateType) -> State {
        State { id, state_type }
    }
}

impl Clone for StateType {
    fn clone(&self) -> StateType { *self }
}

impl Clone for State {
    fn clone(&self) -> State { *self }
}

The struct itself contains only an id and a state-type, which will later be evaluated by the Turing Machine. The only states we will need are: Start, Final and Empty. The Turing machine starts at the Start-state and ends when it reaches the Final state. All Empty states lie in between.

Notice again how I had to implement the Clone trait for both the enum and the class (more on that in a minute).

transition.rs

In the same manner, we can now define the Transition struct which represents the flow from one state of the Turing Machine to the next. It contains the current and next state as well as the current and next char to write to the tape:

Rust
use state;
use direction;

#[derive(Copy)]
pub struct Transition {
    pub current_state: char,
    pub current_char: char,
    pub new_state: char,
    pub new_char: char,
    pub direction: direction::Direction
}

impl Transition {
    pub fn new(current_state: char, current_char: char, 
               new_state: char, new_char: char, direction: direction::Direction) -> Transition {
        Transition { current_state, current_char, new_state, new_char, direction }
    }
}

impl Clone for Transition {
    fn clone(&self) -> Transition { *self }
}

The only method we provide is new to allow us to create new transitions quickly. Beside that, this module doesn’t contain any logic.

tm.rs

With everything set in place, it’s time to take a look at the TM.rs file. This is where all the heavy lifting is done:

Rust
use tape;
use direction;
use transition;
use state;
use std;

pub struct TM {
    states: Vec<state::State>,
    transitions: Vec<transition::Transition>,
    pub tape: tape::Tape
} 

impl TM {
    pub fn new(states: Vec<state::State>, transitions: Vec<transition::Transition>, 
               tape: tape::Tape) -> TM {
        TM { states, transitions, tape }
    }

    pub fn process(&mut self, verbose: bool) { ... }
}

As you can see, our TM struct consists of a list of states, a list of transitions and a tape. All of them will be used by the process method to produce the final output of the Turing Machine, so let’s take a closer look into that:

Rust
pub fn process(&mut self) {
    let mut current_state: state::State = self.get_first_state();
    let mut step: i32 = 0;

    self.log_step(step);

    while current_state.state_type != state::StateType::Final {
        let current_char = self.tape.read();
        let state_id = current_state.id;

        let transition = *self.transitions.iter().clone()
        .find(|transition| transition.current_state == state_id && 
                           transition.current_char == current_char).unwrap();

        current_state = *self.states.iter().clone().find(|state| 
                         state.id == transition.new_state).unwrap();

        step += 1;
        self.tape.write(transition.new_char);
        self.tape.move_head(transition.direction);
        self.log_step(step);
    }
}

First, we initialize a current_state variable and set it to the start state of the Turing Machine. To get this first state, I created the following helper method:

Rust
fn get_first_state(&self) -> state::State {
    let mut iter = self.states.iter().cloned();
    let first_state: Option<state::State> = 
           iter.find(|state| state.state_type == state::StateType::Start);

    match first_state {
        None => panic!("No start state found."),
        Some(state) => state
    }
}

First, we get a mutable iterator and clone it so we can use it without running into borrowing errors. This is also why we had to implement the Clone trait earlier. Then we try to find the first state where the state_type == Start.

If we found a state, it will be returned, otherwise, our code will panic and crash again.

Back in our process method, we initialize a step variable to count the number of steps our Turing machine has taken so far. Then, we log this number as well as the current tape with the log_step method:

Rust
fn log_step(&mut self, step: i32) {
    println!("Tape after step {0}: {1} -> Head position: {2}", step, 
              self.tape.to_string(), self.tape.head_position);
}

Here, we simply print the tape and its head position in a nicely formatted matter.

All the magic, however, happens inside the while loop of the process method:

Rust
while current_state.state_type != state::StateType::Final {
    let current_char = self.tape.read();
    let state_id = current_state.id;

    let transition = *self.transitions.iter().clone()
     .find(|transition| transition.current_state == state_id && 
            transition.current_char == current_char).unwrap();
 
    current_state = *self.states.iter().clone().find
                    (|state| state.id == transition.new_state).unwrap();

   step += 1;
   self.tape.write(transition.new_char);
   self.tape.move_head(transition.direction);
   self.log_step(step);
}

Based on current_char and state_id, we try to find the first matching transition.

Once we have a transition, we will try to find the next state based on the state of that transition. Once this is done, we simply increment the number of steps our Turing Machine has run through, write the new character to the tape and move the head in the according direction.

We also write call our log_step to print the result of the previous step to the console.

This is all we have to do to push the Turing machine to the next state. This step will be repeated until the Turing machine reaches the Final state.

Testing the Turing Machine

Let’s go ahead and visualize this process by testing our machine. To keep it simple, let’s start with the Increment Turing machine I showed you in the beginning. To create such a machine, simply initialize and run it from main.rs:

Rust
mod state;
mod tape;
mod direction;
mod transition;
mod tm;

fn main() {
    let mut tm : tm::TM = increment("$||#");
    tm.process(true);
}

fn increment(word: &str) -> tm::TM {
    let mut tape = tape::Tape::new("$|#", word);
    let states = vec![
        state::State::new('0', state::StateType::Start),
        state::State::new('1', state::StateType::Empty),
        state::State::new('f', state::StateType::Final)
    ];

    let transitions = vec![
        transition::Transition::new('0', '$', '1', '$', direction::Direction::Right),
        transition::Transition::new('1', '|', '1', '|', direction::Direction::Right),
        transition::Transition::new('1', '#', 'f', '|', direction::Direction::None)
    ];

    tm::TM::new(states, transitions, tape)
}

I initialized the tape with the value ‘||’ and added ‘|’ as only valid character (beside $ and #). Then, I defined the states and transitions and used them to instantiate the TuringMachine. Calling its process method should then go through all these states and transitions and write a final ‘|’ at the end of the tape.

If we have done everything correctly thus far, we should see the following output on our console:

Bash
philipp@psrv1 ~/P/r/rust_tm> cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rust_tm`
Tape after step 0: $||# -> Head position: 0
Tape after step 1: $||# -> Head position: 1
Tape after step 2: $||# -> Head position: 2
Tape after step 3: $||# -> Head position: 3
Tape after step 4: $||| -> Head position: 3

Advanced Turing Machines

Now that our Turing machine is up and running, it’s time to add some more interesting machines. We will stick with unary Turing machines and implement one for Decrement, Addition and Subtraction each:

Decrement Machine

The Decrement Machine works very similar to the Increment Machine. However, instead of adding a |, we have to remove the last one after we reach the end of the tape. This means we have to move to the right until we reach the first # character. Then we can simply go one character to the left and remove that |:

Image 3

In Rust, we can represent this Turing machine with the following code:

Rust
...
fn main() {
    let mut tm : tm::TM = decrement("$|||#");
    tm.process(true);
}

fn decrement(word: &str) -> tm::TM {
    let tape = tape::Tape::new("$|#", word);
    let states = vec![
        state::State::new('0', state::StateType::Start),
        state::State::new('1', state::StateType::Empty),
        state::State::new('2', state::StateType::Empty),
        state::State::new('f', state::StateType::Final)
    ];

    let transitions = vec![
        transition::Transition::new('0', '$', '1', '$', direction::Direction::Right),
        transition::Transition::new('1', '|', '1', '|', direction::Direction::Right),
        transition::Transition::new('1', '#', '2', '#', direction::Direction::Left),
        transition::Transition::new('2', '$', 'f', '$', direction::Direction::None),
        transition::Transition::new('2', '|', 'f', '#', direction::Direction::None)
    ];

    tm::TM::new(states, transitions, tape)
}

Running the code now will yield the following result instead:

Bash
cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rust_tm`
Tape after step 0: $|||# -> Head position: 0
Tape after step 1: $|||# -> Head position: 1
Tape after step 2: $|||# -> Head position: 2
Tape after step 3: $|||# -> Head position: 3
Tape after step 4: $|||# -> Head position: 4
Tape after step 5: $|||# -> Head position: 3
Tape after step 6: $||## -> Head position: 3

Addition Machine

Another simple Turing machine we can create is one that adds two numbers. We will represent the addition operation by two unary numbers separated by an &. For example, to calculate the sum of the numbers two and three, we would initialize the tape like this: $||%|||.

To add these numbers, all we have to do is to replace the & with a | and then remove the last |. For the tape above, this would result in $|||||. To realize this, the Turing machine has to go to the right until it reaches the %, replace it with | and then move further to the right until it reaches the end of the tape, marked by a #. Finally, it has to go one character to the left and remove the final |:

Image 4

This translates into the following Rust code:

Rust
...
fn main() {
    let mut tm : tm::TM = add("$|||#");
    tm.process(true);
}

fn add(word: &str) -> tm::TM {
    let tape = tape::Tape::new("$|&#", word);
    let states = vec![
        state::State::new('0', state::StateType::Start),
        state::State::new('1', state::StateType::Empty),
        state::State::new('2', state::StateType::Empty),
        state::State::new('3', state::StateType::Empty),
        state::State::new('4', state::StateType::Empty),
        state::State::new('f', state::StateType::Final)
    ];

    let transitions = vec![
        transition::Transition::new('0', '$', '1', '$', direction::Direction::Right),
        transition::Transition::new('1', '#', 'f', '#', direction::Direction::None),
        transition::Transition::new('1', '|', '1', '|', direction::Direction::Right),
        transition::Transition::new('1', '&', '2', '|', direction::Direction::Right),
        transition::Transition::new('2', '|', '2', '|', direction::Direction::Right),
        transition::Transition::new('2', '#', '3', '#', direction::Direction::Left),
        transition::Transition::new('3', '|', '4', '#', direction::Direction::Left),
        transition::Transition::new('4', '|', '4', '|', direction::Direction::Left),
        transition::Transition::new('4', '$', 'f', '$', direction::Direction::None)
    ];

    tm::TM::new(states, transitions, tape)
}

Running the machine will yield the following result:

Bash
cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rust_tm`
Tape after step 0: $|||&||# -> Head position: 0
Tape after step 1: $|||&||# -> Head position: 1
Tape after step 2: $|||&||# -> Head position: 2
Tape after step 3: $|||&||# -> Head position: 3
Tape after step 4: $|||&||# -> Head position: 4
Tape after step 5: $||||||# -> Head position: 5
Tape after step 6: $||||||# -> Head position: 6
Tape after step 7: $||||||# -> Head position: 7
Tape after step 8: $||||||# -> Head position: 6
Tape after step 9: $|||||## -> Head position: 5
Tape after step 10: $|||||## -> Head position: 4
Tape after step 11: $|||||## -> Head position: 3
Tape after step 12: $|||||## -> Head position: 2
Tape after step 13: $|||||## -> Head position: 1
Tape after step 14: $|||||## -> Head position: 0
Tape after step 15: $|||||## -> Head position: 0

Subtraction Machine

So far, our Turing machines have been fairly trivial. Now let’s tackle a more complicated problem: Subtraction. One way to solve this problem is to move through the tape until we reach the first # (which we will use as Minus operator). Then we have to remove the next | on its right and left side and replace them with #.

For example, given the tape $|||#||#, after these first steps, the tape would look like this: $||###|#. If we repeat this process until there are no more | on the right side of the initial #, the subtraction will be complete. In the case above, we will end up with a tape like: $|#####.

This process will work for tapes of any length. However, since we have to move through an increasing number of # with every step, the Turing machine will take a very long time for large inputs.

As you can see in the image below, the diagram for this Turing machine is much more complicated than the previous ones and it takes some time to fully understand it.

Image 5

In Rust, this is represented by the following code:

Rust
fn main() {
    let mut tm : tm::TM = subtract("$|||#||");
    tm.process(true);
}

fn subtract(word: &str) -> tm::TM {
    let tape = tape::Tape::new("$|#", word);
    let states = vec![
        state::State::new('0', state::StateType::Start),
        state::State::new('1', state::StateType::Empty),
        state::State::new('2', state::StateType::Empty),
        state::State::new('3', state::StateType::Empty),
        state::State::new('4', state::StateType::Empty),
        state::State::new('5', state::StateType::Empty),
        state::State::new('6', state::StateType::Empty),
        state::State::new('7', state::StateType::Empty),
        state::State::new('8', state::StateType::Empty),
        state::State::new('f', state::StateType::Final)
    ];

    let transitions = vec![
        transition::Transition::new('0', '$', '0', '$', direction::Direction::Right),
        transition::Transition::new('0', '#', 'f', '#', direction::Direction::None),
        transition::Transition::new('0', '|', '1', '|', direction::Direction::Right),
        transition::Transition::new('1', '|', '1', '|', direction::Direction::Right),
        transition::Transition::new('1', '#', '2', '#', direction::Direction::Right),
        transition::Transition::new('2', '#', '2', '#', direction::Direction::Right),
        transition::Transition::new('2', '|', '3', '|', direction::Direction::Right),
        transition::Transition::new('3', '|', '4', '|', direction::Direction::Left),
        transition::Transition::new('3', '#', '6', '#', direction::Direction::Left),
        transition::Transition::new('4', '|', '5', '#', direction::Direction::Left),
        transition::Transition::new('5', '#', '5', '#', direction::Direction::Left),
        transition::Transition::new('5', '|', '2', '#', direction::Direction::Right),
        transition::Transition::new('5', '$', '2', '$', direction::Direction::Right),
        transition::Transition::new('6', '|', '7', '#', direction::Direction::Left),
        transition::Transition::new('7', '#', '7', '#', direction::Direction::Left),
        transition::Transition::new('7', '$', 'f', '$', direction::Direction::None),
        transition::Transition::new('7', '|', '8', '#', direction::Direction::Left),
        transition::Transition::new('8', '|', '8', '|', direction::Direction::Left),
        transition::Transition::new('8', '$', 'f', '$', direction::Direction::None)
    ];

    tm::TM::new(states, transitions, tape)
}

Again, running our code yields correct results:

Bash
cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rust_tm`
Tape after step 0: $|||#|| -> Head position: 0
Tape after step 1: $|||#|| -> Head position: 1
Tape after step 2: $|||#|| -> Head position: 2
Tape after step 3: $|||#|| -> Head position: 3
Tape after step 4: $|||#|| -> Head position: 4
Tape after step 5: $|||#|| -> Head position: 5
Tape after step 6: $|||#|| -> Head position: 6
Tape after step 7: $|||#|| -> Head position: 5
Tape after step 8: $|||##| -> Head position: 4
Tape after step 9: $|||##| -> Head position: 3
Tape after step 10: $||###| -> Head position: 4
Tape after step 11: $||###| -> Head position: 5
Tape after step 12: $||###| -> Head position: 6
Tape after step 13: $||###|# -> Head position: 7
Tape after step 14: $||###|# -> Head position: 6
Tape after step 15: $||##### -> Head position: 5
Tape after step 16: $||##### -> Head position: 4
Tape after step 17: $||##### -> Head position: 3
Tape after step 18: $||##### -> Head position: 2
Tape after step 19: $|###### -> Head position: 1
Tape after step 20: $|###### -> Head position: 0
Tape after step 21: $|###### -> Head position: 0

This concludes our collection of Turing machines. I encourage you to play around with them and create new ones. For example, you can try to implement Turing machines for Multiplication and Division.

I hope you enjoyed this article. If you have any questions, problems or feedback, please let me know in the comments.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Germany Germany
Hi there 🙂
My name is Philipp Engelmann, I work as a web developer at FIO SYSTEMS AG in Leipzig. I am interested in C#, Python, (REST-)API-Design, software architecture, algorithms and AI. Check out my blog at https://cheesyprogrammer.com/

Comments and Discussions

 
-- There are no messages in this forum --