Translate

Mostrando las entradas con la etiqueta Q#. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Q#. Mostrar todas las entradas

jueves, 1 de octubre de 2026

Fundamentos de lenguajes de programación cuánticos parte 2

 

Seguimos con : https://emanuelpeg.blogspot.com/2026/10/fundamentos-de-lenguajes-de.html

 ¿Y si el sistema de tipos controlara los qubits?

Acá aparece una conexión muy interesante con un área clásica de los lenguajes de programación: los sistemas de tipos.

Supongamos que tenemos:

qubit q

y hacemos:

measure(q)


Ahora podríamos preguntarnos:

xt

use(q)


¿Es válido?

¿Y qué pasa con:

copy(q)


¿Debería permitirlo el compilador?

Estas preguntas nos llevan a una idea conocida en lenguajes funcionales y sistemas de tipos: los tipos pueden utilizarse para controlar recursos.


Un tipo lineal introduce una restricción interesante: un recurso debe utilizarse exactamente de acuerdo con las reglas establecidas por el sistema de tipos.


Una forma simplificada de imaginarlo sería:

q : Qubit

y determinadas operaciones consumen o transforman ese recurso.


En vez de pensar solamente: variable → valor

podemos pensar: variable → recurso


Esto resulta especialmente interesante para la computación cuántica porque los qubits no se comportan como valores clásicos que podemos copiar libremente.


Por ejemplo, conceptualmente queremos evitar situaciones como:

q = createQubit()

q1 = q

q2 = q


si eso implica que q1 y q2 son copias independientes del mismo estado cuántico.

El sistema de tipos puede ayudar a expresar estas restricciones.


Esta diferencia permite ver algo más profundo.

En muchos lenguajes pensamos principalmente en: valor

Por ejemplo:

x : Int


Pero en determinados lenguajes podemos necesitar pensar también en: t recurso

Un qubit puede considerarse un recurso cuyo uso está restringido.

Esto tiene relación conceptual con otras ideas que ya existen en lenguajes modernos:

  • tipos lineales
  • ownership
  • borrowing
  • affine types
  • resource management


Por supuesto, estas ideas no son idénticas entre sí ni todos los lenguajes cuánticos utilizan exactamente el mismo mecanismo.


Pero comparten una preocupación: controlar qué podemos hacer con determinados valores o recursos.


Los programas cuánticos tampoco necesariamente reemplazan a los programas clásicos.

En muchos casos tenemos una combinación:


programa clásico


       │


       ▼


preparar qubits


       │


       ▼


ejecutar circuito cuántico


       │


       ▼


    medir


       │


       ▼


resultado clásico



Podemos imaginar un programa clásico que decide qué circuito ejecutar:


if condition:

    ejecutarCircuitoA()

else:

    ejecutarCircuitoB()


y dentro de esos circuitos aparecen las operaciones cuánticas.

Esto genera una característica importante de los lenguajes cuánticos: el mundo clásico y el mundo cuántico deben coexistir.


Podemos pensar entonces en dos mundos:

                         Programa

                                │

       ┌────────┴────────┐

       │                                                │

    Clásico                                    Cuántico

       │                                              │

     Int                                           Qubit

     Bool                                        Estado

     List                                         Circuito

     String                                     Medición



El lenguaje debe establecer reglas sobre cómo interactúan ambos.


Por ejemplo:

resultado = measure(q)

convierte información del mundo cuántico en información clásica.


Pero el camino inverso no es simplemente:

q = resultado


Hay que preparar un estado cuántico y aplicar las operaciones correspondientes.


Acá aparece otro punto de contacto con los lenguajes de programación tradicionales.

Podemos imaginar una cadena de compilación como:


Código fuente

     ↓

   AST

     ↓

Representación intermedia

     ↓

Optimización

     ↓

Circuito cuántico

     ↓

Instrucciones

     ↓

Hardware cuántico



Esto es muy parecido a un compilador tradicional.

Pero aparecen problemas nuevos.


Por ejemplo:

¿Podemos eliminar una operación?

 ¿Podemos reordenar dos operaciones?

 ¿Dos circuitos son equivalentes?

 ¿Podemos reducir la cantidad de puertas?

 ¿Cómo traducimos una operación abstracta a las operaciones que soporta un hardware específico?


El compilador cuántico no solamente traduce código.

También puede transformar y optimizar circuitos.


Fundamentos de lenguajes de programación cuánticos


Cuando pensamos en computación cuántica, normalmente aparecen palabras como qubit, superposición, entrelazamiento o puertas cuánticas.

Pero hay otra pregunta interesante:

¿Cómo se programa una computadora cuántica?

Y, más específicamente:

¿Qué cambia en un lenguaje de programación cuando la máquina que ejecuta nuestros programas es cuántica?

La respuesta es interesante porque no alcanza simplemente con agregar un nuevo tipo de dato llamado Qubit.

La computación cuántica introduce restricciones y conceptos que afectan algunas de las ideas fundamentales de los lenguajes de programación: valores, variables, tipos, copia, efectos, recursos y compilación.


En una computadora clásica, la unidad básica de información es el bit.

Un bit puede tener dos valores: 0 o 1

Podemos imaginar entonces una variable:


x = 0


y posteriormente:


x = 1


Un qubit es diferente.

Su estado puede representarse como:


α|0⟩ + β|1⟩


donde α y β son números complejos llamados amplitudes.


No significa simplemente que el qubit contiene un 0 y un 1.

Significa que su estado cuántico es una combinación de ambos estados posibles.


Las amplitudes deben cumplir:


|α|² + |β|² = 1


Cuando medimos el qubit obtenemos un resultado clásico: 0 o 1

La probabilidad de cada resultado depende de las amplitudes.


Por ejemplo:

1/√2 |0⟩ + 1/√2 |1⟩


produce un 0 o un 1 con igual probabilidad.

Esto ya introduce una diferencia importante para un lenguaje de programación.

Una variable clásica tiene un valor.

Un qubit representa un estado cuántico que puede transformarse y posteriormente medirse.


En un lenguaje clásico estamos acostumbrados a operaciones como:

x = x + 1


o:


x = !x


En un lenguaje cuántico aparecen operaciones como las puertas cuánticas.


Por ejemplo:

X(q)


La puerta X tiene un comportamiento similar al NOT clásico sobre los estados básicos:


|0⟩ → |1⟩

|1⟩ → |0⟩


Otra puerta fundamental es H, la puerta de Hadamard.


Aplicada a |0⟩, produce una superposición:


H(|0⟩) = 1/√2 |0⟩ + 1/√2 |1⟩


Es decir, una operación que en un lenguaje clásico podríamos imaginar como una transformación de un valor, en el mundo cuántico transforma un estado.


Una forma habitual de representar un programa cuántico es mediante un circuito.


Por ejemplo:


q ── H ─────●──── M

             │

q ───────────X──── M


Aquí tenemos dos qubits.

Al primero le aplicamos H, después utilizamos ambos en una operación CNOT y finalmente medimos los qubits.

Desde el punto de vista de un lenguaje de programación, esto es bastante interesante.

El programa no solamente describe valores y operaciones. También describe una secuencia de transformaciones sobre estados cuánticos.

Por eso muchos lenguajes y frameworks cuánticos tienen una fuerte relación con la representación de circuitos.


La medición es uno de los conceptos que más claramente rompe nuestras intuiciones clásicas.

Podemos imaginar algo como:

q = |+⟩

donde:

|+⟩ = 1/√2 |0⟩ + 1/√2 |1⟩


Si hacemos: measure(q) obtenemos un resultado clásico: 0 o 1

Pero el proceso de medición también modifica el estado cuántico.


Esto genera una pregunta interesante para el diseño de lenguajes: ¿Qué significa exactamente utilizar nuevamente q después de medirlo?


En un lenguaje clásico, leer una variable normalmente no destruye su valor:


x = 10

print(x)

print(x)

print(x)


En computación cuántica no podemos asumir automáticamente ese comportamiento.

La medición es un efecto que debemos tener en cuenta.


Esta es probablemente una de las diferencias más interesantes desde el punto de vista de los lenguajes de programación.


En un lenguaje clásico podemos hacer:

a = 10

b = a


Tenemos dos valores.

Con un qubit no existe una operación general equivalente a:

q2 = copy(q1)


para copiar arbitrariamente el estado cuántico.


Esto se conoce como el teorema de no clonación cuántica.

No se trata simplemente de que la API de un lenguaje haya decidido prohibir una operación.

Es una propiedad fundamental de la mecánica cuántica.

Y esto tiene consecuencias importantes para el diseño de lenguajes.


Me quedo relargo el post vamos a tener que tener parte 2. 

miércoles, 30 de septiembre de 2026

De C# a Q#: buscando un elemento con Grover


Cuando hablamos de computación cuántica es fácil caer en ejemplos demasiado artificiales. Podemos hablar de superposición, entrelazamiento, qubits e interferencia, pero queda una pregunta bastante más interesante: ¿Cómo se programa realmente un algoritmo cuántico?

Para verlo, vamos a resolver un problema muy sencillo de dos maneras:

Tenemos una colección de elementos y queremos encontrar uno que cumple una determinada condición.


Primero lo hacemos de la manera clásica, con C#. Después veremos cómo abordar el mismo problema con Q# utilizando el algoritmo de Grover.


Supongamos que tenemos:

int[] values = { 3, 8, 12, 17, 21, 42, 55, 61 };


y queremos encontrar el 42.


La solución más sencilla es una búsqueda lineal:


int Search(int[] values, int target)

{

    for (int i = 0; i < values.Length; i++)

    {

        if (values[i] == target)

            return i;

    }


    return -1;

}


No hay ningún misterio.


El algoritmo va recorriendo los elementos:


3  → no

8  → no

12 → no

17 → no

21 → no

42 → sí


En el peor caso tendremos que consultar todos los elementos.

Para una colección de N elementos, esto requiere:


O(N)


consultas.


Acá aparece una diferencia fundamental.


No podemos simplemente escribir:


for cada qubit

    buscar()


y esperar que la computadora cuántica haga lo mismo más rápido.


La programación cuántica utiliza otro modelo.

Uno de los algoritmos más conocidos para búsquedas no estructuradas es Grover's algorithm.


Su idea fundamental es utilizar:

  • superposición
  • un oráculo cuántico
  • interferencia
  • amplificación de amplitud
  • medición


para aumentar la probabilidad de obtener la respuesta que estamos buscando.


La cantidad de consultas necesarias pasa de: O(N)

a aproximadamente: O(√N)


Para utilizar Grover tenemos que formular la búsqueda de otra manera.

En lugar de preguntarnos: ¿Es este elemento igual a 42?

con cada elemento individualmente, definimos una función que identifica la solución.


Por ejemplo:

f(x) = 1  si x es la solución

f(x) = 0  en otro caso


Esta función se implementa como un oráculo cuántico.

Conceptualmente:


                     ┌─────────────┐

x ─────►│          ORACLE           │─────► marca x

                     └─────────────┘


El oráculo no nos devuelve directamente la solución.

Lo que hace es marcarla.

Y después Grover utiliza interferencia para aumentar su amplitud.


Primero necesitamos una superposición

Supongamos que tenemos 8 posibilidades:


000

001

010

011

100

101

110

111


Con tres qubits podemos representar esas ocho posibilidades.

Inicialmente tenemos un estado:

|000>


Aplicando una compuerta Hadamard a cada qubit obtenemos una superposición de todos ellos:


|000> + |001> + |010> + |011>

+ |100> + |101> + |110> + |111>


No significa que tengamos ocho computadoras ejecutándose independientemente.

Significa que nuestro estado cuántico es una combinación de esas posibilidades.


El oracle. Ahora necesitamos marcar nuestra solución.

Supongamos que queremos encontrar: 101


El oracle modifica la fase asociada a ese estado.


Conceptualmente:

000   +

001   +

010   +

011   +

100   +

101   -

110   +

111   +


La solución no apareció mágicamente.

Simplemente fue marcada mediante su fase.

Y esto es importante porque todavía no podemos medir y decir: ¡101!


Si medimos ahora, seguimos teniendo una probabilidad distribuida entre los estados.


Grover aplica una operación llamada difusión o inversión sobre la media.

Su objetivo es modificar las amplitudes de los estados.

Después de una iteración:


solución      ↑↑↑

otros estados ↓


La amplitud de la solución aumenta mientras las amplitudes de los estados incorrectos disminuyen.


Repetimos este proceso aproximadamente: π/4 × √N veces.


Finalmente medimos.

La probabilidad de obtener la solución es mucho mayor.


¿Y dónde está Q#?

Ahora viene la parte que más nos interesa.

Q# no intenta esconder que estamos trabajando con un modelo diferente.


Un programa cuántico puede trabajar con Qubit: use qs = Qubit[3];

Tenemos tres qubits.

Podemos ponerlos en superposición:

ApplyToEach(H, qs);


H es la compuerta Hadamard.

Después podemos aplicar nuestro oracle y la operación de difusión.

Una versión conceptual de Grover puede verse así:


operation Grover(qs : Qubit[]) : Unit {

    ApplyToEach(H, qs);


    // Oracle

    Oracle(qs);


    // Difusión

    Diffusion(qs);

}


En una implementación real necesitamos definir cuidadosamente el oracle y la difusión, pero la estructura fundamental ya está ahí:


superposición

      ↓

   oracle

      ↓

  difusión

      ↓

   medir


Comparemos las dos soluciones.

En C# escribimos:


for (int i = 0; i < values.Length; i++)

{

    if (values[i] == target)

        return i;

}


El programa piensa en términos de:


elemento

elemento

elemento

elemento

...

Es una búsqueda secuencial.


En Q# pensamos en:


qubits

  ↓

superposición

  ↓

oracle

  ↓

interferencia

  ↓

medición


No estamos escribiendo una versión más complicada del mismo for.

Estamos utilizando un modelo computacional diferente.

Sería muy fácil concluir: Entonces una computadora cuántica puede buscar en una lista de un millón de elementos en √1.000.000 = 1.000 pasos.


No es tan sencillo.

Grover proporciona una ventaja en el número de consultas al oracle.

Eso no significa que toda la aplicación se ejecute en O(√N).


Tenemos que:

  • preparar el estado cuántico;
  • construir el oracle;
  • ejecutar las operaciones cuánticas;
  • repetir Grover el número adecuado de veces;
  • realizar la medición.


Además, las computadoras cuánticas reales tienen limitaciones físicas y de hardware.

Por eso es más correcto decir: Grover reduce el número de consultas necesarias para una búsqueda no estructurada de O(N) a O(√N).

No significa que cualquier programa de búsqueda pueda reemplazarse automáticamente por Grover.


Lo interesante de Q# no es simplemente que tenga un tipo llamado Qubit.

Lo interesante es que el lenguaje permite expresar conceptos que no aparecen en un lenguaje clásico tradicional:

  • Qubit
  • Hadamard
  • superposición
  • medición
  • operaciones unitarias
  • oracle


Y eso cambia la forma en que pensamos el algoritmo.

En C# pensamos:

datos → operaciones → resultado


En Q# podemos pensar:


estado cuántico

      ↓

transformación

      ↓

interferencia

      ↓

medición

      ↓

resultado



No podemos decir simplemente eso.

Estamos comparando dos modelos de computación diferentes.


Para nuestra búsqueda:

Clásico: O(N)


frente a Grover: O(√N) consultas al oracle


Pero para aprovechar esa ventaja necesitamos una computadora cuántica y un problema que pueda expresarse adecuadamente mediante un oracle.

Además, si los datos están almacenados en una memoria clásica y tenemos que cargarlos en la computadora cuántica, esa carga también forma parte del problema.


Lo verdaderamente interesante

Quizás la parte más interesante de este ejemplo no sea que:

O(N) → O(√N)


sino por qué es posible hacerlo.


Una computadora clásica trabaja con bits:

0

1


Una computadora cuántica trabaja con qubits, que pueden encontrarse en superposición:

α|0> + β|1>


y las operaciones cuánticas pueden modificar las amplitudes de esos estados.

Grover aprovecha precisamente esto.

No estamos ejecutando un for` más rápido.


Estamos diseñando un algoritmo alrededor de las propiedades de la mecánica cuántica.

Y acá aparece una cuestión interesante para los lenguajes de programación.

En C# podemos abstraernos completamente del hardware:

int result = Search(values, 42);


En Q#, en cambio, muchas de las abstracciones del lenguaje están relacionadas directamente con el modelo cuántico:

use qs = Qubit[3];


ApplyToEach(H, qs);


Oracle(qs);

Diffusion(qs);


El programador tiene que pensar en:

¿Qué estado tengo?

¿Qué transformación estoy aplicando?

¿Qué amplitudes quiero aumentar?

¿Qué amplitudes quiero cancelar?

¿Cuándo puedo medir?


Es decir, el paradigma de programación cambia.


Grover es un buen ejemplo para entender que la computación cuántica no consiste simplemente en ejecutar programas clásicos más rápido.


Tenemos el mismo problema: encontrar un elemento que cumple una condición.


Pero utilizamos dos modelos diferentes.

En el modelo clásico:


buscar

   ↓

comparar

   ↓

buscar

   ↓

comparar

   ↓

...



En el modelo cuántico:


superposición

      ↓

    oracle

      ↓

 interferencia

      ↓

amplificación

      ↓

   medición


Y ahí está, quizás, la diferencia más importante entre programar para una computadora clásica y programar para una computadora cuántica: No se trata solamente de aprender nuevas instrucciones. Hay que aprender a pensar en términos de estados cuánticos y transformaciones sobre esos estados.


martes, 29 de septiembre de 2026

Quicksort en Q#


Un algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en Erlang, Rust, haskell y lisp


Ahora le toca a Q#, el lenguaje de programación lógica/relacional. Básicamente, el algoritmo toma un pivote y agrupa los menores que el pivote al principio y los mayores al final y aplica quicksort a estos dos grupos. Y si la lista es vacía o tiene un elemento, ya está ordenada. 


Vamos al código: 


namespace QuantumQuickSort {


    function QuickSort(xs : Int[]) : Int[] {

        if Length(xs) <= 1 {

            return xs;

        }


        let pivot = xs[0];


        let smaller = Filter(x -> x <= pivot, xs[1...]);

        let greater = Filter(x -> x > pivot, xs[1...]);


        return QuickSort(smaller)

               + [pivot]

               + QuickSort(greater);

    }


    function Filter(predicate : (Int -> Bool), xs : Int[]) : Int[] {

        mutable result = [];


        for x in xs {

            if predicate(x) {

                set result += [x];

            }

        }


        return result;

    }

}