Writing a Turing Machine in Haskell

ottermad

Charles Thomas

Posted on January 5, 2022

Writing a Turing Machine in Haskell

As I've been learning Quantum Computing, I've had to learn some Computer Science along the way. One of the concepts I'd heard of but I hadn't understood until recently was that of a Turing Machine.

To understand what a Turing Machine was, I decided to write one in Haskell.

Setting up the project

To follow along with this project the first thing you'll need to do is get Haskell installed. To do this visit this page. And follow the instructions there to install Haskell and Cabal (the Haskell package manager)

Once, you've installed Haskell, we need to set up the directory structure we'll be using (take a look at this Github repo for reference). Create a directory called TuringMachineHaskell. This will be the root of our project.

Inside that directory create a folder called src. Then src/Setup.hs and add:

import Distribution.Simple
main = defaultMain
Enter fullscreen mode Exit fullscreen mode

Also inside the src directory create a file called Turing.hs. This is where most of our code will live and add this at the top of the file:

module Turing where
Enter fullscreen mode Exit fullscreen mode

Outside of src but still inside TuringMachineHaskell create the file TuringMachineHaskell.cabal. Add the following code to set up the tests:

cabal-version:       2.4

-- Initial package description 'TuringMachineHaskell.cabal' generated by
-- 'cabal init'.  For further documentation, see
-- http://haskell.org/cabal/users-guide/

-- The name of the package.
name:                TuringMachineHaskell

-- The package version.  See the Haskell package versioning policy (PVP)
-- for standards guiding when and how versions should be incremented.
-- https://pvp.haskell.org
-- PVP summary:      +-+------- breaking API changes
--                   | | +----- non-breaking API additions
--                   | | | +--- code changes with no API change
version:             0.1.0.0

-- A short (one-line) description of the package.
-- synopsis:

-- A longer description of the package.
-- description:

-- A URL where users can report bugs.
-- bug-reports:

-- The license under which the package is released.
license:             NONE

-- The file containing the license text.
license-file:        LICENSE

-- The package author(s).
author:              

-- An email address to which users can send suggestions, bug reports, and
-- patches.
maintainer:          

-- A copyright notice.
-- copyright:

-- category:

-- Extra files to be distributed with the package, such as examples or a
-- README.
extra-source-files:  CHANGELOG.md


executable TuringMachineHaskell
  -- .hs or .lhs file containing the Main module.
  main-is:             Main.hs

  -- Modules included in this executable, other than Main.
  -- other-modules: Turing

  -- LANGUAGE extensions used by modules in this package.
  -- other-extensions:

  -- Other library packages from which modules are imported.
  build-depends:       base ^>=4.14.3.0

  -- Directories containing source files.
  hs-source-dirs: src

  -- Base language which the package is written in.
  default-language:    Haskell2010
Enter fullscreen mode Exit fullscreen mode

Modelling the machine

To model the machine we're going to add code to Turing.hs

State

The first thing a Turing Machine has is a series of states it can move between. In our case, are two special states we need to make a note of:

  • The start state which is the state it starts in
  • The halt state which is the state it finishes in

We are going to represent these and 3 other generic states with the following line:

data State = Halt | StartState | A | B | C deriving (Eq, Show)
Enter fullscreen mode Exit fullscreen mode

The Tape

The next thing a Turing machine has is a tape and on each square on the tape is a symbol. Let's represent this as:

data Symbol = Start | Zero | One | Blank deriving (Eq, Show)
type Tape = [Symbol]
Enter fullscreen mode Exit fullscreen mode

Instructions

A Turing machine also has a list of instructions. In a Turing Machine, the instructions have 5 main parts.

The first two parts of the instructions are a symbol and a state. They are used to work out if we should apply an instruction. If the current state of the Turing machine and the current symbol on the tape match these two, then the instruction is applied.

The three parts of instructions are used when that instruction is applied:

  • A new state to put the machine into
  • Aa new symbol to write to the current position on a tape
  • An indication of whether to move the tape forward a square, back a square or to keep it in the same place.
data PositionShift = Backwards | Forwards | Stay deriving (Show)

data Instruction = Instruction { 
    stateToMatch :: State, 
    symbolToMatch :: Symbol, 
    newState :: State, 
    newSymbol :: Symbol, 
    positionShift :: PositionShift 
} deriving (Show)
Enter fullscreen mode Exit fullscreen mode

The Machine

Now we can represent the Turing machine itself. We need to keep track of it all the states it could be in, its current state, the tape and the current position on the tape and the instructions.

data TuringMachine = TuringMachine { 
    states :: [State], 
    currentState :: State, 
    tape :: Tape, 
    currentPosition :: Int, 
    instructions :: [Instruction] 
} deriving (Show)
Enter fullscreen mode Exit fullscreen mode

The Logic

The first part of the logic of our Turing machine is to:

  • Look at its current state
  • If it's in a halted state, end the program
  • If not we try and see if there is an instruction we can process.
runMachine :: TuringMachine -> TuringMachine
runMachine machine
    | (currentState machine) == Halt = machine
    | otherwise  = runMachine (machineCycle machine)
Enter fullscreen mode Exit fullscreen mode

Here the machineCycle handles trying to find an instruction to run, if it finds one, it runs it, otherwise it sets the machine to the Halt state.

machineCycle :: TuringMachine -> TuringMachine
machineCycle machine = 
    case instructionToApply of 
        Just instruction -> applyInstruction machine instruction
        Nothing -> haltMachine machine
    where instructionToApply = findInstructionToApply machine (instructions machine)

haltMachine :: TuringMachine -> TuringMachine
haltMachine machine =
    TuringMachine {
        states = states machine, 
        currentState = Halt, 
        tape = tape machine, 
        currentPosition = currentPosition machine,
        instructions = instructions machine
    }
Enter fullscreen mode Exit fullscreen mode

Looking for an instruction

To find an instruction to apply it recursively searches the list of instructions:

findInstructionToApply :: TuringMachine -> [Instruction] -> Maybe Instruction
findInstructionToApply machine instructions
    | null instructions = Nothing
    | shouldApplyInstruction machine (instructions !! 0) =  Just (instructions !! 0)
    | otherwise = findInstructionToApply machine (tail instructions)
Enter fullscreen mode Exit fullscreen mode

If the instructions array is empty it returns 0, otherwise, it checks the first instruction in the list. If it should be applied it returns it. Otherwise, it calls itself with the remaining instructions.

To determine if an instruction should be applied we use this function. It compares the current state and symbol of the machine to the instruction

shouldApplyInstruction :: TuringMachine -> Instruction -> Bool
shouldApplyInstruction machine instruction = 
    (currentState machine == stateToMatch instruction) && (currentSymbol machine == symbolToMatch instruction)

currentSymbol :: TuringMachine -> Symbol
currentSymbol machine = (tape machine) !! (currentPosition machine)
Enter fullscreen mode Exit fullscreen mode

Applying an instruction

Now we need a function to apply the instruction when we've found one:

applyInstruction :: TuringMachine -> Instruction -> TuringMachine
applyInstruction machine instruction =
    TuringMachine {
        states =  states machine, 
        currentState = newState instruction, 
        tape = newTape (tape machine) (currentPosition machine) (newSymbol instruction), 
        currentPosition = getNewPosition (machine) (positionShift instruction),
        instructions = instructions machine
    }

-- Given a tape, a position in the tape to update and a symbol it creates a new tape
newTape :: Tape -> Int -> Symbol -> Tape
newTape tape position newSymbol =
    take (position) tape ++ newSymbol : drop (position+1) tape

-- This takes a position and a shift and returns a new position
getNewPosition :: TuringMachine -> PositionShift -> Int
getNewPosition machine Forwards = (currentPosition machine) + 1
getNewPosition machine Stay = (currentPosition machine)
getNewPosition machine Backwards 
    | (currentPosition machine) == 0 = 0
    | otherwise = (currentPosition machine) - 1
Enter fullscreen mode Exit fullscreen mode

Creating a new machine

Finally, we have a helper that creates a new Turing with an infinite tape:

newTuringMachine :: Tape -> [Instruction] -> TuringMachine 
newTuringMachine tape instructions = TuringMachine{
        states = [Halt, StartState, A, B, C],
        currentState = StartState,
        tape = [Start] ++ tape ++ repeat Blank,
        currentPosition = 0,
        instructions = instructions
    }

Enter fullscreen mode Exit fullscreen mode

Testing it

We can create a test by creating a machine that always outputs one. Create a file called Main.hs inside src and add the code:

module Main where

import Turing

main :: IO ()
main = do
    let myMachine = newTuringMachine [One, Zero, One] [
            Instruction{stateToMatch = StartState, symbolToMatch=Start, newState = A, newSymbol = Start, positionShift = Forwards},
            Instruction{stateToMatch = A, symbolToMatch=Zero, newState = A, newSymbol = Blank, positionShift = Forwards},
            Instruction{stateToMatch = A, symbolToMatch=One, newState = A, newSymbol = Blank, positionShift = Forwards},
            Instruction{stateToMatch = A, symbolToMatch=Blank, newState = B, newSymbol = Blank, positionShift = Backwards},
            Instruction{stateToMatch = B, symbolToMatch=Blank, newState = B, newSymbol = Blank, positionShift = Backwards},
            Instruction{stateToMatch = B, symbolToMatch=Start, newState = C, newSymbol = Start, positionShift = Forwards},
            Instruction{stateToMatch = C, symbolToMatch=Blank, newState = Halt, newSymbol = One, positionShift = Stay}
            ]
    print (take 10 (tape myMachine))
    let outputMachine = runMachine myMachine
    print (take 10 (tape outputMachine))
Enter fullscreen mode Exit fullscreen mode

We can run this file by going inside TuringMachineHaskell in a terminal and running: cabal run

Understanding the machine

Let's go through and understand what this Turing Machine does.

The first instruction takes the machine from the initial state into a state A and moves the tape forward once (to the first symbol of our input).

The next two instructions say while our machine is in state A replace any 1 or 0 with a blank and move the tape forward once.

The fourth instruction is when it reads its first blank. It sees a blank so we know we're at the end of the input. So we put the machine into state B and moves the tape back one square.
The instruction then says if we're in state B which happens when we've processed the whole input. If we're on a blank square go backwards. This will set up back the beginning of the tape.

The sixth instruction, says if in state B and can see a start symbol (e.g. we're back at the start of the tape) then set the state to C and move forward one square.

The final instruction says to write one to the blank square and halt.

So in summary, this machine:

  • reads the input replacing the 1s and 0s with blanks until it reaches a blank square (the end of the input)
  • reverses the tape until it gets to the start,
  • moves forward one square, write 1 to the tape and halts Toucan Over 100 word limit We’re working to increase this limit and keep load times short. In the meantime, try highlighting up to 100 words at one time to translate. Don’t show again
💖 💪 🙅 🚩
ottermad
Charles Thomas

Posted on January 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Parsers are relative bimonads
haskell Parsers are relative bimonads

August 20, 2024

Writing a Turing Machine in Haskell
haskell Writing a Turing Machine in Haskell

January 5, 2022

Indiscernible types of Haskell Holmes
haskell Indiscernible types of Haskell Holmes

November 5, 2020