I recently found out about the Quipper quantum programming language (hat tip to @senorflor) and was inspired to write a Clojure library that allows playing with Qubits at the repl. In contrast to my quantum circuits app, which only allows ahead-of-time design of entire circuits, this library lets you create qubits on the fly and interact with them in an open-ended way.
The part I thought was the most interesting was how appropriate common object-oriented techniques were for modeling the qubits. Some of the worst aspects of popular object-oriented design are hidden state and unexpected side effects. But both of these turn out to be perfect for qubits. Qubits have more state information than is possible to extract (so it’s in a sense fundamentally hidden), and the act of observing them will generally change their state. Not only that, if the qubit is entangled with other qubits, observing one will change the state of the others as well.
For modeling this in Clojure I used a
deftype with a ref in one of its fields (none of the code here includes the actual math of quantum computation, which is buried away in the
(deftype Qubit [name system]) (defn init-system "Creates a singleton system with the qubit's value 0, and sets the qubit's ref to that system." [^Qubit q] (let [system (data/single-qubit-system q 0)] (dosync (alter (.system q) (constantly system))))) (defn qubit "Creates a fresh qubit, with a name if supplied." ( (qubit (gensym "qubit-"))) ([name'] (doto (->Qubit (name name') (ref nil)) (init-system))))
When two qubits become entangled, their internal “systems” are merged, and both of their refs are pointed to the same merged system, which is what allows them to have internal references to each other:
(defn update-system-pointers! "Given a system-map, updates all the .system refs of the :qubits list to point to that map." [system] (doseq [^Qubit q (:qubits system)] (alter (.system q) (constantly system)))) (defn merge-systems! "Updates the system properties of the qubits so that they are all together." [qs] (dosync (let [systems (distinct (map (fn [^Qubit q] @(.system q)) qs))] (when (> (count systems) 1) (let [system (reduce data/merge-systems systems)] (update-system-pointers! system)))))) (defn single-qubit-gate-fn "Given a gate definition [[a b] [c d]], returns a function that takes a primary qubit and optional control qubits and executes the gate on it." [gate] (fn [^Qubit q & controls] (dosync (when (seq controls) (merge-systems! (cons q controls))) (let [new-system (data/apply-single-qubit-gate gate @(.system q) q controls)] (update-system-pointers! new-system))) q))
Observation is just as side-effecting as applying a gate:
(defn observe "Returns 0 or 1." [^Qubit q] (dosync (let [[outcome new-system] (data/observe @(.system q) q)] ;; if the qubit was previously entangled, detangle it (if (> (count (:qubits new-system)) 1) (let [new-system-1 (data/factor-qubit-from-system new-system q) new-system-2 (data/single-qubit-system q outcome)] (update-system-pointers! new-system-1) (update-system-pointers! new-system-2)) (update-system-pointers! new-system)) outcome)))
I doubt this approach would be useful for doing anything serious related to quantum computation, but for the sake of learning how qubits behave I think it could be promising. The rest of the code is on Github.