So we now have a structure HeapMap that can be used to represent the base states in the description of a quantum state, and a structure VecEqL that can be used to define a sum of these base states, along with their corresponding amplitudes. As such, we can now define the specific instance of the VecEqL type that we shall be using to represent the pure quantum states in our computations. Specifically, a Pure state is described by a VecEqL structure over the HeapMap structure, along with complex numbers C to represent the amplitudes as would be expected.
typePure = VecEqL C HeapMap
We can now think of a unitary operator in terms of the effect it has on each base state within a pure state. In fact, we can define the type Unitary to represent our unitary operators in such a way. The extra Int argument represents the next available qubit, just as we needed to keep track of it for the classical simulator.
dataUnitary = U {unU :: Int → HeapMap → Pure }
When evaluating the application of a QIO unitary (in U ), these new Unitary functions are mapped over each base state in the whole pure state of the system.
The results of this mapping are then joined using the underlying ⊕ operation defined for the Pure data-type. The approach we take to evaluate our QIO com- putations in this way is to define a member of the Unitary data-type for every corresponding primitive member of the U datatype. So that the monoidal be- haviour of the U data-type can also be modelled in this way, we must define a monoidal behaviour for the Unitary data-type. This monoidal behaviour makes use of the underlying (embedded ) monadic behaviour of the Pure data-type.
instanceMonoid Unitary where • = U (λfv h → unEmbed (return h))
(U f ) (U g) = U (λfv h → unEmbed (do h′ ← Embed (f fv h)
h′′ ← Embed (g fv h′)
return h′′))
With all the structure in place, we can now define the actual function runU , which lifts members of the U data-type to their corresponding Unitary function.
runU :: U → Unitary
runU UReturn =•
runU (Rot x a u) = uRot x a runU u runU (Swap x y u) = uSwap x y runU u
runU (Cond x us u) = uCond x (runU ◦ us) runU u runU (Ulet b xu u) = uLet b (runU ◦ xu) runU u
The functions uRot, uSwap, uCond and uLet actually convert each of the underlying U type into a Unitary. For example, the uRot function is as follows:
uRot :: Qbit → Rotation → Unitary uRot q r = (uMatrix q (r (False, False),
r (False, True), r (True, False), r (True, True))) uMatrix :: Qbit → (C, C, C, C) → Unitary
if (fromJust (h ? q))
then (m01 ⊗ (unEmbed (return (update h q False)))) ⊕ (m11 ⊗ (unEmbed (return h)))
else (m00 ⊗ (unEmbed (return h)))
⊕ (m10 ⊗ (unEmbed (return (update h q True))))))
We can look now at defining how the other monadic constructors of QIO are dealt with during evaluation. In order to do this, we must first define a data structure that can be used to keep track of the state of our system. This data structure (StateQ) consists of a member of the Pure data-type, that corresponds to the current quantum state, and an integer that represents the next free qubit. We again must keep track of the next available qubit so that the system can assign a new qubit when necessary.
dataStateQ = StateQ {free :: Int, pure :: Pure }
With our StateQ structure defined, we can look at how the monadic construc- tors of QIO are evaluated. As we can now think of this evaluation as a stateful computation, we are able to use Haskell’s State monad, as was introduced in sec- tion 4.4.1, to define our evaluation functions. We will look shortly at what the PMonad class requirement is, but for now we can just think of it as providing a merge function that describes how measurements and results are presented back to the user, and we’ll see how the two instances of PMonad we provide relate to the only differences between the sim and run functions. This evaluation function is called evalWith as a state is stored implicitly by the use of the State monad, and the evalWith function can be thought of as evaluating the given QIO computation with that state.
evalWith :: PMonad m ⇒ QIO a → State StateQ (m a) The QReturn primitive is just the return of the monad.
evalWith (QReturn a) = return (return a)
The MkQbit qubit primitive updates the overall state by increasing the free variable, and updating the pure state with a qubit initialised in the state corre-
sponding to the given Boolean
evalWith (MkQbit b g) = do (StateQ f p)← get put (StateQ (f + 1)
(updateP p (Qbit f ) b)) evalWith (g (Qbit f ))
The ApplyU primitive uses the runU function as described above, and updates the current state accordingly.
evalWith (ApplyU u q) = do (StateQ f p)← get
put (StateQ f (unEmbed ( dox ← Embed (p) x′ ← Embed (uu f x ) return x′ ))) evalWith q whereU uu = runU u
The Meas primitive is evaluated by splitting the Pure state about the given qubit, creating two Pure states that represent the state of the system for each of the possible measurement outcomes. The computation then proceeds over both of these Pure states, giving a pair of results. The merging of these two results is left to the merge function of the underlying PMonad . We shall look at the definition of a PMonad shortly, and how this merge function defines the behaviour of the overall evaluation.
evalWith (Meas x g) = do (StateQ f p)← get
(let Split pr ift iff = split p x in if pr < 0∨ pr > 1
thenerror "pr < 0 or >1" else doput (StateQ f ift)
pift ← evalWith (g True) put (StateQ f iff )
piff ← evalWith (g False) return (merge pr pift piff ))
We’ve seen how the behaviour of the evaluation function depends on the merge function provided by an underlying PMonad structure. In fact, we define a PMonad as just a Monad along with this extra merge function.
classMonad m ⇒ PMonad m where
merge :: R → m a → m a → m a
The merge function can be thought of as a function that defines how two compu- tations of the same type can be combined, depending on a real argument to that function. This real argument represents the probability of each of the computa- tions occurring, and as such, we are able to define instances of the PMonad that give rise to the two different behaviours of our simulation functions (sim and run). In fact, to be more specific, the real argument represents the probability of the first computation occurring (p), and the probability of the second computation occurring can be calculated as 1− p.
For the run function, we define the IO monad as an instance of a PMonad . To do this, we define the merge function to make use of the IO monad’s random number generator to choose one of the two merged computations to perform, probabilistically depending on the given probability.
instancePMonad IO where
merge pr ift iff = Random.randomRIO (0, 1.0)
>>= λpp → if pr > pp then ift else iff
For the sim function however, we define a type constructor that represents a probability distribution over any type a. This type constructor is similar to the VecEq type constructor introduced for quantum states, but doesn’t have the requirement that the underlying type is a member of Eq, and hence doesn’t keep the vectors normalised in the same way.
dataProb a = Prob{unProb :: Vec R a }
same as for the VecEq type constructor, just without the normalisation behaviour. The R values that correspond to each member of the vector are the probabilities of each member in the distribution. So that we can use this structure in our merge operation, we must define this Prob type constructor as an instance of the PMonad class, and this first entails that we first define it as an instance of the Monad class.
instanceMonad Prob where
return = Prob ◦ return
(Prob ps) >>= f = Prob (ps >>= unProb◦ f )
We can now define the corresponding merge operation, and note how it gives rise to the probability distributions when used by the sim function.
instancePMonad Prob where
merge pr (Prob ift) (Prob iff ) = Prob ((pr ⊗ ift)
⊕ ((1 − pr) ⊗ iff ))
The merge function multiplies each of the underlying probability distribution ar- guments by their respective probabilities, and then joins these to create the new probability distribution, as gained by the result of a measurement.
Lazy evaluation is specifically useful here as if the merge function only requires one of the Pure states (as is the case for the run function), then the other Pure state is never evaluated.
We can finish this section by giving the final definitions for the run and sim functions. In fact, they both call the same eval function (which calls evalWith over an empty initial state). The types given for each of the simulator functions inform the type-system which PMonad should be used in the evaluation of the QIO computations.
eval :: PMonad m ⇒ QIO a → m a
eval p = evalState (evalWith p) initialStateQ run :: QIO a → IO a
sim :: QIO a → Prob a sim = eval
This chapter has so far introduced an implementation of the QIO monad in Haskell. The next section goes on to discuss some of the pitfalls of this implemen- tation, and lays out the case for reimplementing QIO in a stronger type-system, as is the case in chapter 9.