Mostrando entradas con la etiqueta lenguajes de programación. Mostrar todas las entradas
Mostrando entradas con la etiqueta lenguajes de programación. Mostrar todas las entradas

sábado, 13 de diciembre de 2014

miniSL parte 24 - Expresiones infijas

Seguimos estudiando el analizador sintáctico de miniSL. Hemos empezado con las expresiones de mayor precedencia. Primarias en la parte 22. Postfijas en la 23. Prefijas también en la 23. Ahora vamos a ver las infijas.

Si bien hasta ahora cada tipo de expresión tenía una única precedencia, con las infijas esto no es así. Una expresión infija puede tener una precedencia distinta a la de otra expresión infija. La precedencia de cada expresión infija viene dada por los operadores que la componen. De hecho, hemos visto ya el algoritmo para analizar las expresiones infijas en este blog. Es el algoritmo de playa de agujas.

En miniSL la función que implementa este algoritmo es ReadInfixExpression(). En esta versión del algoritmo usamos la recursividad. No es la versión más eficiente, pero sí es la más sencilla de entender. La idea es mantener siempre la precedencia de la expresión que estamos analizando. Si el siguiente operador es de menor precedencia, no lo analizamos y dejamos que sea la función que nos ha llamado la que se encargue. Obviamente, la primera llamada deberá ser con la precedencia más baja posible.

CELL& Script::ReadInfixExpression(ISTREAM& i, int precedence)
{

En la variable a vamos a mantener la expresión analizada hasta ahora que tendrá la precedencia dada en precedence o una mayor. Empezamos con una expresión prefija que es justo la de mayor precedencia tras las infijas, por lo que aseguramos esta condición.

CELL* a=&ReadPrefixExpression(i);

Ahora vemos que el siguiente token de la entrada sea un operador. Es decir, un T_SYMBOL. Si no es un operador, el análisis de la expresión no puede continuar y finalizamos. En este bucle continuaremos mientras vayamos encontrando operadores. También debemos tener en cuenta que si lo que nos encontramos no es un operador, no debemos consumir ese token ya que otra función podría necesitarlo para continuar el análisis léxico. Por eso usamos peek().

TOKEN t;
while((t=PeekToken(i)).type==T_SYMBOL)
{

En este punto sabemos que estamos tratando con un operador. Obtengamos qué operador es, qué precedencia tiene y qué asociatividad.

STRING const& s=*t.data->string_val;
int prec=SymbolPrecedenceAndAssoc(s);
bool right_assoc=(prec&1)!=0;

Ahora viene la decisión comentada anteriormente y núcleo del algoritmo. Si la precedencia del operador es menor que la de la expresión actual (precedencia que tenemos almacenada en el parámetro precedence), retornamos lo que hayamos analizado hasta ahora y la función que nos ha llamado deberá hacerse cargo de ese operador con una precedencia tan baja. En el caso de que el operador sea exactamente de la misma precedencia que la expresión que estoy analizando, deberemos decidir si lo aglutinamos en esta expresión (por lo que continuamos el algoritmo aquí) o en la expresión superior (por lo que retornamos a la función llamante). Esto depende de la asociatividad del operador como veremos algo más abajo.

if(right_assoc ? prec<precedence : prec<=precedence)
return *a;

Una vez que hemos decidido que debemos procesar este operador, lo consumimos de la entrada.

ReadToken(i);

A continuación leemos el resto de la expresión con precedencia mayor (o igual en el caso de asociatividad por la derecha) que el operador recién leído. Cuando se encuentre con una precedencia menor, volverá a esta función y seguiremos comparando con la precedencia de precedence.

CELL& b=ReadInfixExpression(i, prec);

Llegados a este punto, tenemos dos expresiones y un operador. Construimos las celdas correspondientes. La celda a se aglutina por la izquierda y la celda b por la derecha. El resultado se vuelve a almacenar en a. Esto significa que siempre asociamos por la izquierda en este bucle. Por esto mismo retornábamos en caso de precedencias iguales si queríamos asociar por la derecha.

CELL& operands=CreateCell(CONS_CTOR, a, &CreateCell(CONS_CTOR, &b, &CreateCell(EMPTY_LIT)));
a=&CreateCell(COMBINE_CODE, t.data, &operands );
}

Este retorno es la salida cuando encontramos un token que no es un operador y salíamos del bucle.

return *a;
}

Con esta función completada sólo nos falta implementar la función que lee listas. Esta función ReadList() ha aparecido ya varias veces usada en las expresiones primarias y postfijas. La describiremos en la siguiente parte de esta serie.

domingo, 15 de junio de 2014

miniSL Parte 23 - Expresiones postfijas y prefijas

Esta es otra parte de la serie dedicada al mini-lenguaje MiniSL. Tal como veníamos haciendo en las partes anteriores, vamos a seguir ascendiendo por las reglas de analizador descendente recursivo. La regla que usa las expresiones primarias es la de las expresiones postfijas. Esta regla está implementada por la función ReadPostfixExpression().

CELL& Script::ReadPostfixExpression(ISTREAM& i)
{

Siguiendo nuestra sintaxis, una expresión postfija es una expresión primaria.

 CELL* p=&ReadPrimaryExpression(i);

Seguida de ninguna, una o varias llamadas de función. Estas llamadas deben empezar forzosamente por el carácter “(”. No podemos consumirlo porque si no es una llamada a función, podría ser otra cosa importante como un operador infijo. Así que sólo ojeamos el siguiente token con PeekToken(). Este procedimiento de mirar por adelantado es muy usual en el reconocimiento sintáctico y se denomina lookahead (mira hacia delante). Se detalló el funcionamiento de PeekToken() en la parte 17 de esta serie.

 TOKEN t=PeekToken(i);
 while(t.type==T_RAW && t.raw=='(')
 {

Una vez que estamos seguros de que lo que tenemos es una llamada a función, la consumimos. Primero el paréntesis de apertura. Luego leemos la lista de argumentos en una celda especial de combinación (como se explicó en la parte sexta de esta serie). Finalmente, consumimos el paréntesis de cierre.

  t=ReadToken(i);
  p=&CreateCell(COMBINE_CODE, p, &ReadList(i, ',', ')'));
  t=PeekToken(i);
 }

 return *p;
}

Lo que generamos es un árbol en el cual cada nodo es una celda de combinación (COMBINE_CODE) que representa una llamada a función y en sus argumentos añadimos una lista. Veremos las listas en la sección 25, pero ya sabemos que las listas se construyen con celdas del tipo CONS_CTOR y EMPTY_LIT.


Para la función ReadPrefixExpression(), lo que haremos será lo siguiente.

CELL& Script::ReadPrefixExpression(ISTREAM& i)
{
 if(PeekToken(i).type==T_SYMBOL)
 {

Si encontramos un símbolo, lo consumimos y llamamos recursivamente a esta función para el argumento de la función prefija.
  TOKEN t=ReadToken(i);
  CELL& expr=ReadPrefixExpression(i);

El resultado lo guardamos como un único argumento (una única celda CONS_CTOR) en una llamada (celda COMBINE_CODE).

  return CreateCell(COMBINE_CODE, t.data, &CreateCell(CONS_CTOR, &expr, &CreateCell(EMPTY_LIT)));
 }

Si no empezábamos por un símbolo es que hemos llegado a la expresión postfija.

 return ReadPostfixExpression(i);
}

Estas funciones son simples y lo único que hacen es seguir la gramática. De hecho, estas funciones son la parte más representativa de lo que es un reconocedor sintáctico descendiente recursivo. En la siguiente parte veremos algo un poco distinto y usaremos el algoritmo de playa de agujas para leer expresiones infijas.

sábado, 24 de agosto de 2013

¿Qué es eso de la programación asíncrona?

Llamadas síncronas

Gran parte del tiempo de las operaciones que realiza un programa en su ejecución se pierde esperando. Esperando que los datos lleguen del disco. Esperando que el usuario pulse una tecla. Esperando que el paquete se mande por red. Esperando, esperando y esperando.

La solución clásica a este problema es proporcionarle ese tiempo de espera a otro proceso de manera que se aproveche el uso de CPU. Nuestro proceso se bloquea mientras espera (y si va a esperar mucho incluso se quita de memoria y guarda en disco) y otro toma su lugar. Cuando llega la señal de que la espera ha terminado (y se queda la CPU libre) nuestro proceso se reanuda.

Existen diversas políticas de reparto del tiempo de CPU entre los distintos procesos para que este reparto sea lo más útil posible. Por ejemplo, primando al proceso con el que el usuario está trabajando.

Ahora bien. Desde el punto de vista del programa, ¿qué significa este sistema? Significa que cuando la llamada a una función que realiza una operación termina, la operación se ha completado y que, implícitamente, nuestro programa ha estado esperado a que terminase.

Debido a que la terminación de la operación y la reanudación del programa ocurren a la vez, se dice que estamos realizando llamadas síncronas.

f=leer_fichero()
//Hasta que no se lee el fichero no llegamos aquí
t=leer_teclado()
//Hasta que no se lee el teclado no llegamos aquí
buscar(f,t)
//Resto del programa

Llamadas asíncronas

La alternativa es no esperar, ser asíncronos. Debido a que no esperamos ocurren dos cosas:
  1. No bloqueamos la ejecución de nuestro programa. Por esta razón a las llamadas asíncronas también se les dice no bloqueantes y a las síncronas, bloqueantes.
  2. La llamada retorna antes de que se complete la operación y, por tanto, no puede devolver el resultado.
Bien. El punto 1 era lo requerido, ¿pero cómo solucionamos el punto 2? Existen varias opciones.
  • Pipes: conectar de alguna manera la operación con otra antes de solicitar su realización.
  • Polling: preguntar cada cierto tiempo a ver si se ha completado la operación. No suele ser buena idea ya que perdemos tiempo en preguntar.
  • Interrupción: esperar a una señal y que nuestro programa o el sistema ejecute, en el momento que se recibe la señal, un código en respuesta a la misma. Es importante ver aquí que este código está desligado del punto en el que se solicitó la operación mediante la llamada asíncrona.
  • Eventos/mensajes: es una mejora sobre polling. Cuando termina la operación se envía un mensaje que se encola. El programa no pregunta si una operación en concreto se ha completado. Sólo comprueba si ha llegado algún mensaje. Esto le indica que una operación de las muchas en curso se ha completado. Cada mensaje tiene un indicador de la operación completada y deberá ser respondido correspondientemente, de forma similar a como se hacía con las interrupciones.
  • Hebras o fibras: nos rendimos. Usamos llamadas síncronas, pero en vez de que bloqueen mi programa, doy opciones de por dónde se puede continuar la ejecución. En el caso de las hebras es el sistema operativo el que decide cómo planificar la ejecución, en el caso de la fibra es mi propio programa el que lo hace.
  • Sincronizar (p.ej. por futuros/promesa): nos rendimos definitivamente. Seguimos ejecutando como si nada mientras no necesitemos el resultado. En el momento que necesitemos el resultado, esperamos hasta tenerlo.
  • Retrollamadas: como interrupción, pero indico qué código hay que ejecutar en el momento de realizar la llamada asíncrona. Esto tiene el efecto de bifurcar el flujo de control y analizaremos esto en lo que resta de artículo.
Seguro que hay más métodos para solucionar el punto 2, pero o son menos conocidos o no los recuerdo ahora. Es más, de todos los métodos mencionados arriba los más usados son los cuatro últimos y, aún así, los mensajes empiezan ya a parecer algo antiguo. Destruyen la lógica del programa que se tiene que repartir en trocitos entre los distintos manejadores de eventos. Cualquiera que haya programado en win32 sabrá de lo que hablo.

Por otro lado, tanto sincronizar como las hebras/fibras es rendirse y esperar.


Retrollamadas

Lo que realmente está en auge son las retrollamadas (callbacks), sobre todo desde que C# incluyera soporte en el lenguaje para la programación asíncrona. Pero vayamos por partes. ¿Qué es esto de una retrollamada?

Usaremos el ejemplo de arriba.

f=leer_fichero()
t=leer_teclado()
buscar(f,t)
//Resto del programa

Con retrollamadas, en vez de esperar el resultado de las funciones, indico a estas funciones qué tienen que hacer cuando terminen.

leer_fichero_asincrono(usa_fichero)
leer_teclado_asincrono(usa_teclado)

//Resto del programa

Así, en el caso de leer_fichero_asincrono() lo que hago es decirle que, cuando termine de leer el fichero llame a la función usa_fichero(). Igualmente, con leer_teclado_asincrono(). Estas funciones usa_fichero() y usa_teclado() que se ejecutan cuando se completa la operación son las retrollamadas.

Acabadas las presentaciones, lo importante aquí es que la función  leer_fichero_asincrono() no espera y la ejecución continúa. Se ejecuta leer_teclado_asincrono() y tampoco espera, seguimos ejecutando lo que haya que ejecutar despues y, mientras, se lee del fichero y del teclado a la vez.

¿Y dónde está la función buscar() que es donde realmente hacemos algo con lo leído? Bueno, no podemos llamarla hasta que estemos seguros de que tanto el fichero como el teclado se han leído. Esa es la responsabilidad de usa_fichero() y usa_teclado() que deben guardar los datos, comprobar que están ambos y, si es así, llamar la función buscar() con ellos.

var f, t string;
func usa_fichero(rf) { f = rf; if t!="" {buscar(f,t)} }
func usa_teclado(rt) { t = rt; if f!="" {buscar(f,t)} }

//Resto del programa

Para eso guardamos los datos recibidos en dos variables visibles por ambas retrollamadas y cuando se comprueba que ambas cadenas se han leído, llama a buscar().


Funciones anónimas

El ejemplo anterior es algo engorroso ya que hay que definir una función por cada retrollamada que se quiera usar. Este hecho ha sido determinante para que no se usaran mucho las retrollamadas. Por fortuna, los lenguajes de programación van evolucionando y se han ido adaptando ideas que venían de lenguajes menos conocidos y académicos. Una de esas ideas son las funciones anónimas y sus clausuras léxicas.

Con funciones anónimas el código del ejemplo anterior se reduce lo suficiente como para que sea atractivo su uso.

var f, t string;

leer_fichero_asincrono(rf => { f = rf; if t!="" {buscar(f,t) } )
leer_teclado_asincrono(rt => { t = rt; if f!="" {buscar(f,t) } )

//Resto del programa

Aunque no hemos solucionado el detalle de que la función buscar() aparece escrita dos veces aunque sólo se llamará una. En este ejemplo es una pequeña tontería, pero en ejemplos mayores el grado de verbosidad aumenta y se dificulta el mantenimiento del programa.

Quiero resaltar aquí que una función asíncrona bifurca el flujo de control ya que van a ocurrir dos cosas a la vez: la operación que haga la función (se hará la retrollamada cuando se complete) y el resto del programa (ya que la función retorna inmediatamente). De manera que si escribo algo como esto:

asincrona( x => parte1(x) )
parte2()

La parte 1 y la parte 2 se ejecutan, a la vez o en momentos distintos, pero no en secuencia. Es más, una vez que la parte 1 se haya completado, ese flujo de control bifurcado muere sin hacer nada más. La parte 2 que es el resto del programa seguirá su ejecución hasta que muera cuando el proceso acabe.


Composición de retrollamadas

En el ejemplo anterior se hacían a la vez tres cosas:
  • La lectura del fichero
  • La lectura del teclado
  • La ejecución del resto del programa 
Hemos realizado una composición en paralelo de las operaciones. ¿Qué pasa si por la razón que sea debo leer el teclado después de leer el fichero? El ejemplo cambiaría tal y como sigue:

var f, t string;
leer_fichero_asincrono(f => { leer_teclado_asincrono(t => buscar(f,t)) })

//Resto del programa

En este caso lo que tenemos es una composición en serie (o secuencia) de las operaciones. Se hacen a la vez dos cosas:
  • La lectura del fichero y luego la del teclado
  • La ejecución del resto del programa
Si bien la composición en paralelo introducía duplicación de código y la necesidad de usar variables auxiliares, la composición en serie introduce el anidamiento sintáctico de las expresiones. En los programas reales ambos efectos conllevan un aumento de la verbosidad que se ha dado en llamar el infierno de las retrollamdas.


La solución del café helado

La manera más simple que he visto de resolver estos defectos es la que usa el lenguaje Iced Coffee Script. Básicamente introduce dos nuevas palabras clave await y defer. La palabra clave await marca el resto del código como una función anónima (que llamaré función anónima restante) y la palabra clave defer indica el punto donde introducir esa función anónima como retrollamada.

Empezaremos por un ejemplo más simple que sólo lee del teclado e imprime por pantalla. No sigo la sintaxis de Iced Coffee Script, sólo las ideas.

await leer_teclado_asincrono(defer(t))
imprime(t);

La palabra clave await toma el resto del código (que en este caso sólo es imprime(t)) y crea una función anónima con él. Esta función anónima sería algo como  t => imprime(t). Luego, sustituye en los defer esa función anónima. El resultado de esta traducción sintáctica (como si fuera una macro) es

leer_teclado_asincrono( t => imprime(t) )

Verdaderamente, hemos hecho el código más largo, pero hemos sacado la retrollamada del argumento de la función asíncrona y por tanto no estamos anidando estructuras sintácticas una dentro de otra. Si tuvieramos que componer secuencialmente varias llamadas es inmediato.

await leer_teclado_asincrono(defer(t))
await leer_fichero_asincrono(defer(f))
await leer_datos_asincrono(defer(d))
imprime(t,f,d)

Mientras que con retrollamadas sería un pequeño lío.

leer_teclado_asincrono(t => {
  leer_fichero_asincrono(f => {
    leer_datos_asincrono(d => {
      imprime(t,f,d)
    })
  })
})

Adicionalmente, el uso de la palabra clave defer permite la composición en paralelo.

await {
  leer_teclado_asincrono(defer(t))
  leer_fichero_asincrono(defer(f))
  leer_datos_asincrono(defer(d))
}
imprime(t,f,d)

Recordemos que al llamar a cualquiera de las tres funciones asíncronas, el programa no se detiene. Sólo espera (justo lo que significa await) cuando llega al final de bloque } y una vez se hayan completado las operaciones, se sigue con imprime().

Traducir esto a retrollamadas es bastante más complicado ya que necesitaríamos variables auxiliares y triplicar el código de llamada a imprime() o usar otra función auxiliar.


Continuaciones

El uso de await nos ha ocultado uno de los dos flujos de datos. Es fácil de recuperar si envolvemos todo en una función.

funcion lee_e_imprime_asincrono(){
  await leer_teclado_asincrono(defer(t))
  imprime(t)
}

lee_e_imprime_asincrono()
//Resto del código

Debo recordar aquí la versión traducida de lee_e_imprime _asincrono () que es

funcion lee_e_imprime_asincrono(){
  leer_teclado_asincrono( t => imprime(t) )
}

Lo hago para dejar claro que esta función es una función que retorna inmediatamente ya que llama a leer_teclado_asincrono() que hemos dicho que no esperaba a que se completase la lectura, sino que retornaba inmediatamente. Por eso la hemos nombrado con _asincrono al final.

Como retorna inmediatamente, se ejecuta el resto del código a la vez que se lee el teclado y se llama a imprime(). De nuevo, tenemos los dos flujos de control.

Ahora bien, si la función que acabo de crear es asíncrona... ¿por qué no acepta una retrollamada? La respuesta es que ¡debería hacerlo! Si no lo hace, desde el punto de uso sólo tenemos acceso a uno de los dos flujos de control.

lee_e_imprime_asincrono() //Falta acceso al flujo que pasa por imprime()

//Resto del código (obviamente tenemos acceso a este flujo desde aquí)

De hecho, esa retrollamada que deberíamos aceptar sería lo que hay que hacer una vez hayamos terminado con la lectura del teclado y sus retrollamadas asociadas (en este caso sólo es llamar a imprime()). Es decir, el acceso que nos falta al otro flujo de control.

funcion lee_e_imprime_asincrono(k funcion){
  await leer_teclado_asincrono(defer(t))
  imprime(t)
  k() //Continua ejecutando k
}

Ahora sí podemos componer por el otro flujo de control.

lee_e_imprime_asincrono(()=>imprime("hecho"))

//Resto del código

Este estilo de llamar a las funciones, donde manipulamos el flujo de control pasando funciones que deben ejecutarse una vez acabada las tareas que realice la función que llamamos, es el estilo de paso de continuaciones.


La alternativa C#

El problema del estilo de paso de continuaciones es que una función tiene una signatura distinta si es síncrona  o asíncrona.

funcion_sincrona(x,y,z)
funcion_asincrona(x,y,z, continuacion)

Además, nos obliga a que la continuación se conozca por anticipado. No podemos cambiarla. No podemos controlar la operación. Una vez que llamamos a la función asíncrona, perdemos el control.

Una manera de solucionarlo es, en vez de pasar la continuación, reificar la operación asíncrona y devolver un objeto que la represente. Este objeto acepta la continuación y una serie de operaciones que controlan la operación que se está realizando asíncronamente.

En C# ese objeto es de la clase Task. Esta clase permite definir la continuación con los métodos ContinueWith. Además, controla la operación que realiza la función que creó este objeto por ejemplo, esperando síncronamente, retardando, etc.

El ejemplo anterior (con la sintaxis que hemos seguido hasta ahora) usando esta idea sería:

funcion lee_e_imprime_asincrono(){
  await leer_teclado_asincrono(defer(t))
  imprime(t)
}

lee_e_imprime_asincrono().establece_continuacion(()=>imprime("hecho") )

//Resto del código

Como todo tiene su lado negativo, esta técnica dificulta la composición en paralelo. Hay que usar métodos explícitos y no se podrían poner varios defer (de hecho, C# ni siquiera usa defer). Es decir, se tiene que gestionar a mano la composición en paralelo de los objetos asíncronos.



Notas finales


Quedan algunos detalles que no voy a discutir. Este artículo se está alargando más de la cuenta. Por ejemplo, ¿cómo se llaman a las retrollamadas? ¿Quién lo hace? ¿Es el sistema operativo? ¿El propio programa?

Es más, ¿tenemos problemas de carreras con las retrollamadas? ¿Tengo que sincronizar el acceso?

Todo esto depende de la plataforma, pero en general se intenta que la retrollamada se realice desde la misma hebra y en ciertos puntos controlados para que no haya este tipo de problemas.

Otro tema que ha quedado en el aire es la definición formal de transformación de las palabras clave await/defer en otra función que usa clausuras. Realmente, sólo se usa una clausura y una máquina de estados. Esto es así porque un bloque await puede aparecer dentro de un bucle. En este caso lo que hay después del await vuelve delante con el bucle. ¿Cuál debe ser entonces la clausura?

También hemos dejado de lado ver qué ocurre con las excepciones o cancelaciones de una llamada asíncrona.

Quizás tratemos estos temas en otro post más adelante.

domingo, 19 de mayo de 2013

Álgebras y coálgebras

He encontrado varias veces por Internet la pregunta de qué es un coálgebra y las respuestas han sido más o menos liosas. El motivo es que en computación al hablar de (co)álgebras nos referimos a las F-(co)álgebras. Lo que implica explicar qué es la F y esa F es un funtor. Un funtor es una operación en categorías y hay que introducir parte de la teoría de categorías para explicarlo.

Puede hacerse más simple.

El producto cartesiano generalizado

Estrictamente hablando el producto cartesiano es una operación entre dos conjuntos.[$$A\times B$$]El resultado es otro conjunto y sus elementos son los pares ordenados de forma que el primer elemento se obtenga del primer conjunto y el segundo elemento del segundo. En notación matemática:[$$A\times B = \{ (a,b) \mid a\in A, b\in B \}$$] Cuando hacemos dos productos cartesianos, surge la duda de si es asociativo o no.[$$A\times (B \times C) \overset{?}{=} (A\times B)\times C$$] En general el producto cartesiano no es asociativo, ya que [$(a,(b,c))\ne((a,b),c)$], pero nada impide usar un producto cartesiano generalizado (que escribiré entre paréntesis) en el cual el producto de tres conjuntos no sean pares, sino triplas. [$$(A\times B \times C) = \{ (a,b,c) \mid a\in A, b\in B, c\in C \}$$] El de cuatro conjuntos serían cuadruplas y el de [$n$] conjuntos lo que llamaremos [$n$]-tuplas. [$$(A_1\times\cdots\times A_n) = \{(a_1,\dots,a_n) \mid a_1\in A_1,\dots,a_n\in A_n \}$$] Usaremos este producto cartesiano generalizado.

Antes de acabar este apartado sólo resta destacar un caso particular. El del producto cartesiano de cero conjuntos. Llamaremos a este conjunto [$1$] y sólo contiene un elemento que es la tupla vacía [$()$].

Esta definición no debe ser extraña si seguimos la siguiente secuencia. [$$(A\times B \times C) = \{ (a,b,c) \mid a\in A, b\in B, c\in C \}$$][$$(A\times B) = \{ (a,b) \mid a\in A, b\in B \}$$][$$(A) = \{ (a) \mid a\in A \}$$][$$1 = \{ () \}$$]

Algebras

La idea de usar álgebras proviene de tomar un tipo de datos como sus valores y las operaciones que los construyen por inducción. Por ejemplo, en el caso de una lista, estos constructores serían.
  • La lista vacía. Lo que se ha venido a llamar la constante nil.
  • Añadir un elemento a la lista. Función que se llama cons.
Las constantes se expresan como funciones que no toman argumentos (mejor dicho, toman la tupla vacía), por lo que si la lista es de enteros y llamo al tipo de esta lista [int], el tipo de cada una de estas funciones sería: [$$nil : 1 \to [int]$$]Que no toma argumentos y devuelve la lista de enteros vacía y[$$cons : (int \times [int]) \to [int]$$]Que toma un entero y una lista de enteros y devuelve otra con este elemento añadido.

Teniendo en mente estas funciones, decimos que el tipo lista de enteros [int] no es más que el conjunto de sus valores (que llamaré [$A_{[int]}$]) y las dos funciones que los construyen por inducción. [$$(A_{[int]}, nil : 1\to[int] , cons : (int \times [int]) \to [int] )$$]Muy similar a, por ejemplo, un monoide [$$(M,e:1 \to M ,\otimes:(M\times M)\to M )$$] donde [$M$] son los elementos del monoide, [$e$] el elemento neutro y [$\otimes$] la operación interna. O a la definición de los números naturales[$$(N,cero:1\to N, suc:N \to N)$$] donde [$cero$] es la constante cero y [$suc$] la operación que obtiene el siguiente número natural (el sucesor).

Generalizando de nuevo, vemos que todas estas definiciones tienen siempre la misma estructura.[$$(A, f_1: X_1 \to A,\dots, f_n: X_n \to A)$$]Donde los [$X_i$] son productos cartesianos de conjuntos en los cuales puede estar el propio [$A$] o no.

Bien. Eso es un álgebra. Un conjunto y varias operaciones cuyo codominio es tal conjunto.

Coálgebras

En un coálgebra lo que tenemos es [$$(A, f_1: A\to X_1,\dots, f_n: A\to X_n)$$] En este caso las operaciones tienen el conjunto en su dominio.

¿Qué significado tiene esto?

Pensemos en una lista. La operación cons tomaba una lista y un elemento e introducía ese elemento en la lista.

cons(7,[1,2,3])=[7,1,2,3]
cons(9,[8,1,3])=[9,8,1,3]
cons(4,[])=[4]

El tipo de cons era [$(int\times [int])\to [int]$]. En una coálgebra la operación correspondiente (que llamaremos headtail) deberá tener el tipo [$[int]\to(int\times [int])$] lo que significa que de una lista obtenemos un elemento y otra lista. Una forma de ver esto es pensar que, en la coálgebra, la operación headtail extrae un elemento de la lista. En general, las operaciones de una coálgebra deshacen lo que los constructores hacían en las álgebras. Por esta razón a las operaciones de una coálgebra se las denomina destructores.

headtail([1,2,3])=(1,[2,3])
headtail ([8,1,3])=(8,[1,3])
headtail ([])=????

¿Qué pasó en este último ejemplo? ¿Es realmente headtail una función sobre las listas o sólo sobre las listas no vacías?

Existen dos respuestas aquí.

La primera respuesta es introducir los tipos suma. De esa manera, headtail podría devolver un error sumando el conjunto unidad al tipo de su codominio ([$headtail: [int]\to 1+(int\times [int])$]). Esto es dual a la agrupación de todas las operaciones del álgebra de la lista en una única operación sumando sus dominios ([$nilcons: 1+(int\times [int])\to [int]$]) y por esta vía definimos el funtor polinómico [$F X = 1+(X\times [X])$] y llegamos a las F-(co)álgebras que es de lo que realmente estamos hablando.

Aún introduciendo la posibilidad de que headtail acabe de leer una lista, no significa que lo haga. Usar coinducción permite trabajar con listas infinitas en las cuales siempre podemos ir extrayendo elementos. Esto nos lleva a...

La segunda respuesta es modificar nuestro tipo de datos y decir que no trabajamos con listas, sino con listas infinitas (los llamados streams). Como la lista es infinita, nunca va a estar vacía y siempre podremos usar headtail. Estos tipos de datos infinitos definidos por coálgebras y coinducción se denominan codatos. Son útiles para modelar, por ejemplo, la entrada del usuario que, en principio, nunca acaba.

sábado, 2 de febrero de 2013

Coma serial, agujeros de gusano, gotos desnudos, e implícitos por tipo

Funciones

Llamemos a una función. Una función que está en un lugar distante del código.
Si sigo el flujo de control (dibujado en rojo abajo) y el flujo de datos (en azul), ambos van paralelos. Es decir, el valor de a (dato) se lleva a la función doble (control) y se trae de vuelta el resultado (dato) al retornar (control).
La variable a es local. Eso quiere decir que fuera de mi trocito de código no se ve. Por eso necesitamos pasar la variable a a la función doble. ¿Qué pasaría si a fuera global y sí se viese en todo el programa incluida la función doble?

Variables globales

En este caso, no hace falta pasar la variable global a ninguna función, puesto que ya se ve desde allí.
Ahora, una vez guardado el dato 3 en la variable global a, está disponible en cualquier punto del programa. No hace falta pasarlo como argumento a la función doble. El flujo de datos a través de la variable global a se ha ocultado.

Ahora bien, ¿qué ocurre si otro trozo de código ejecuta doble en vez del que estoy usando como ejemplo?
Primero, el flujo de control llegará y asignará el dato 3 a la variable global a. Luego, el programa seguirá funcionando. Algún tiempo o mucho tiempo después, el flujo de control llegará al trozo de código en el que se ejecuta doble y se recupera el valor de la variable global a.

La variable global ha hecho que el dato 3 se pase de una punta a la otra del programa sin que haya relación directa entre esos dos trozos de código. Si no vemos la definición de doble, el uso de la variable global a dentro de doble no es tan obvio. No aparece ni en sus parámetros ni en sus argumentos. El efecto es el de un dato que se teleporta de un lugar a otro del programa.

Beam me up, Scotty!


Entra en un lugar del universo y sale por otro completamente distinto. Como los agujeros de gusano, pero mucho peor ya que puede haber más de dos extremos en una variable global.

Que alguien busque un error en un programa lleno de variables globales con datos que se teleportan de un lado a otro sin relación directa es muy, muy difícil.

Conclusión lógica: mejor evitar el uso de variables globales porque separan el flujo de datos del flujo de control.

Se considera al goto dañino

Hay ríos de tinta (y de bits) explicando que la instrucción goto debe ser evitada a toda costa y, además, su uso es síntoma de mal código. Muchas veces se toma demasiado al pie de la letra el título del artículo de Dijkstra que puso este problema sobre la mesa, pero sin terminar de leer el artículo por completo y viendo el porqué de la mala fama del goto.

Realmente no siempre el goto es malo. Veamos la razón. El goto es el caso complementario a una variable global. En la variable global era el flujo de datos el que saltaba de una punta a otra del código, con el goto es el flujo de control el que salta.
Como goto no permite pasar datos al lugar donde salta, el flujo de datos se acaba en el goto y empieza en el lugar donde llega. Este lugar es llamado la etiqueta. Llamaremos a este tipo de goto un goto desnudo.

¿Y si quiero pasar datos desde un goto desnudo hasta su etiqueta? Muy sencillo: debemos usar las variables globales.
Es más, si usamos gotos desnudos, no nos queda otra opción que usar variables globales. Las variables globales implican saltos en el flujo de datos: lo que entra en una variable global puede aparecer en la otra punta del código. Además, los gotos implican saltos en el flujo de control: la etiqueta de destino de un goto puede estar en la otra punta del código. Las dos cosas juntas hacen que la comprensión de un programa no trivial sea imposible.

Conclusión: los gotos desnudos son malos porque nos fuerzan a usar variables globales. Realmente, la raíz de todo mal está en las variables globales ya que los gotos pueden modificarse para que no sea necesario el uso de estas variables.

Restricción de saltos

¿Cómo podemos restringir el uso de los goto para que sea seguro? La primera lección la da el lenguaje C: un goto sólo puede saltar a una etiqueta que esté en su misma función.

void g(void)
{
y:
  puts(“ups!”);
}

void f(void)
{
  int a;
x:
  a=rand();
  if(a==0) goto x;  //OK: x está en f
  else     goto y;  //ERROR: y está en otra función
  printf(“%d\n”,a);
}

De esta forma antes y después del goto están visibles las variables locales de la función. No hace falta usar variables globales ya que la comunicación se realiza mediante variables locales.

Otra lección proviene de la teoría de lenguajes y consiste en añadir al goto un parámetro. Llamaremos a este goto vestido.

goto x(3);

Y en la etiqueta recogemos el argumento.

x(a):
print(a);

La etiqueta se comporta en cierta manera como una función que nunca retorna. Esto se acerca al concepto de continuación. También, con las continuaciones, pasamos los datos como si fuera una función y no necesitamos variables globales.

Evitamos la raíz de todo mal: las variables globales y sus agujeros de gusano.

Nota: Una continuación es un valor de forma similar a lo que las clausuras son a las funciones, por lo que el goto vestido no es exactamente una continuación.

Implícitos

Sin embargo, no tener variables globales es tedioso a la hora de escribir un programa porque hay que ir arrastrando el contexto que antes mantenía la variable global.

Propongamos este simple ejemplo:

Dato d; //Mi dato d que voy a usar como variable global

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b)
}
int main()
{
  //Prepara x e y
  //Preparo mi dato d
  f(x,y)
}

La función h usa d. Por ese motivo se dice que d es contexto de h. Como d es global no tenemos problemas en escribirla en cualquier parte del código, incluida h. ¿Qué pasaría si d no pudiera ser global? Tendríamos que ponerla en main(), pasársela a f y propagarla hasta llegar a h. Es decir, tenemos que escribir el contexto en los parámetros de las funciones que lo usan.

Esto es justamente lo que deseamos hacer: que el flujo de datos y de control vayan paralelos.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  f(x,y,d)
}

En las funciones f y g hemos tenido que añadir el dato para transmitirlo hasta la función h. Cada vez que usemos f o g, hemos de añadir d ¡y eso pueden ser muchas, muchas veces! ¿Cómo solucionarlo sin variables globales?

La mejor forma que he visto es mediante argumentos implícitos. Una característica del lenguaje de programación Scala. Consiste en añadir la palabra clave implicit en los parámetros que se comportan como propagadores de contexto.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  f(x,y,d)
}

Hasta ahora nada cambia, pero declarar un parámetro como implícito significa que es opcional y, si no se pone, se busca el llamado valor implícito de ese tipo de datos en el entorno actual. La forma de declarar un valor implícito es también con la palabra clave implicit.

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b,d)
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  implicit d; //Usa d como el valor implícito para Dato
  f(x,y) //El implícito para el tipo Dato es d.
         //Entonces, es como llamar a f(x,y,d)
}

Nota: ¿Por qué Scala asocia un valor implícito al tipo Dato y no, por ejemplo, a cada nombre de manera que si no escribo el argumento implícito busque la variable con nombre “d”? ¿No es muy rebuscado buscar el valor implícito según sea el tipo del parámetro? La respuesta es bastante teórica. Este mecanismo introduce una dependencia de valores desde los tipos y esto es muy útil para otras muchas cosas como los conceptos (también llamados clases de tipos).

Lo interesante es que un parámetro implícito es directamente el valor implícito en el entorno de la función, por lo que tampoco hace falta escribir g(a,b,d).

void h(Dato d); //La función que usa el dato
void g(OtrosDatos a, YMasDatos b, implicit Dato d)
{
  //Esta función hace otras cosas con a y b pero, al final usa h
  h(d)
}
void f(AlgunasCosas x, OtrasCosas y, implicit Dato d)
{
  //Esta función hace cosas con x e y, pero al final llama a g
  g(a,b) //d es implícito aquí también
}
int main()
{
  //Prepara x e y
  Dato d; //Mi dato d que voy a usar como variable local
  //Preparo mi dato d
  implicit d; //Usa d como un implícito para Dato
  f(x,y) //El implícito para el tipo Dato es d.
         //Entonces, es como llamar a f(x,y,d)
}

Por supuesto, en este ejemplo sólo nos hemos ahorrado escribir d dos veces, pero en un programa real suele haber muchos más usos de este tipo de parámetros. Así que con estos parámetros implícitos se soslaya el inconveniente de no poder usar variables globales.

La coma serial

Una bagatela para finalizar. El título de esta entrada se llama "Coma serial, agujeros de gusano, gotos desnudos, e implícitos por tipo". Un lector atento habrá visto una escritura inusual de la enumeración. Concretamente usando la coma antes de la conjunción.

Este tipo de uso de la coma no es común en el español, pero en inglés sí que lo es. Se denomina la coma de Oxford, la coma de Harvard o la coma serial. Se usa para evitar ambigüedades como estas:

"Dedicado a mis padres, Juan y Ana." Bien. Los padres son Juan y Ana.
"Dedicado a mis padres, Juan, y Ana." Mmmm.... Los padres no son Juan y Ana. ¿Verdad?

En el caso del título podía confundirse "e implícitos por tipo " como si los goto fueran, además de desnudos, implícitos por tipo. No es el caso. Los implícitos por tipo son otra cosa. Para evitar la ambigüedad, usé la coma serial.

viernes, 28 de octubre de 2011

El combinador Y en C++ (y II)

La primera parte de esta serie está aquí.

Como se comentó en la primera parte, el combinador Y toma una función g que representa un paso de recursión y devuelve una función p que es el punto fijo de g. Esto del punto fijo no es más que la función recursiva obtenida aplicando g a sí misma infinitas veces. El problema con este planteamiento es que currificar en C++ es difícil. Por esta razón usaremos una versión de g que no esté currificada. El prototipo de esta función sería:
#include <functional>

int g(std::function<int(int)> f,int v);
El tipo de los objetos función en C++ es muy largo y escribir "std::function" cada vez va a emborronar el código. Usaré una macro para aligerar la notación.
#include <functional>

//F(X,Y) es X->Y
#define F(X,Y) std::function<Y(X)>

//F(X,Y,Z) es (X,Y)->Z
#define F2(X,Y,Z) std::function&lg;Z(X,Y)>

int g(F(int,int) f,int v);
Ahora vamos a tratar de definir el combinador Y. La mejor forma de hacerlo es, en vez de usar las funciones lambda de C++ que son muy tediosas, usando la ecuación recursiva que obtuvimos en la primera parte.

Y(g)=g(Y(g))

Como ahora tengo a g sin currificar, he de añadir un argumento extra. Queda así:

Y(g)=[](x){g(Y(g),x)}

Entonces, el código del combinador Y para los enteros sería

F(int,int) Y(  F2( F(int,int) ,int,int)  f)
{
    return [=](int x){return f(Y(f),x);};
}

Realmente la parte difícil es la de los tipos, aunque se pueden obtener poco a poco estudiando la expresión.

Cambiar los int por un tipo genérico nos da el combinador Y genérico en C++:

template<typename T>
F(T,T) Y(  F2( F(T,T) ,T,T)  f)
{
    return [=](T x){return f(Y(f),x);};
}

Ahora sólo tenemos que usarlo. Definimos para eso la función g del factorial y lo volcamos a la salida. El código final es el que sigue:

#include <functional>

#include <iostream>

//F(X,Y) es X->Y
#define F(X,Y) std::function<Y(X)>
 

//F(X,Y,Z) es (X,Y)->Z
#define F2(X,Y,Z) std::function&lg;Z(X,Y)>

int g(F(int,int) f,int v)
{
    return v==0 ? 1 : v * f(v-1);
}

template<typename T>
F(T,T) Y(  F2( F(T,T) ,T,T)  f)
{
    return [=](T x){return f(Y(f),x);};
}

int main(int argc,char** argv)
{
std::cout << Y<int>(g)(5) << std::endl;
    return 0;
}

En este punto sólo queda advertir al lector que es necesario usar un compilador compatible con el estándar de C++ de 2011.

jueves, 29 de septiembre de 2011

Todas las funciones están en CPS

Hace algún tiempo hablé del CPS y las continuaciones. Había un detalle que hacía completamente incompatible el estilo CPS con el estilo normal de programación: toda función en CPS requiere un parámetro extra que es la continuación. Una función normal se definiría más o menos así:

doble(x) = x+x

Al hacer el cambio a CPS, y suponiendo que la suma es una operación predefinida que no se pasa a CPS, obtendríamos lo siguiente:

doble_CPS(x,k) = k(x+x)

La nueva función requiere ese argumento extra que es la continuación k. La continuación es una función que no regresa y que representa el resto de la computación. Lo que vamos a hacer con el resultado del doble.

Nota: El que una continuación no regrese es muy interesante desde el punto de vista de la teoría de tipos. Su tipo sería [$T\rightarrow \bot$] que es igual a [$¬ T$], pero dejaré una explicación más profunda para otra entrada más adelante.

Bien. Ahora pensemos qué pasa si tengo un lenguaje de programación en el cual la compilación va a un ensamblador genérico. Una llamada a la función doble, en ensamblador, sería algo así:

PUSH x
CALL doble

Primero insertamos el parámetro en la pila y luego llamamos a doble. Si la llamada fuera la versión CPS tendríamos algo así:

PUSH x
PUSH k
CALL doble_CPS

Esto impediría que un lenguaje cuyo compilador transformase el código a CPS interactuase con otro que no usase la transformación a CPS. En un caso se espera un argumento y en el otro dos.

Sin embargo, esto no es así. Prácticamente todos los procesasores reales (incluyendo el x86) son de estilo CPS y hay que decir, en cada llamada, cuál es la continuación. ¿Cómo? Observando con detenimiento la instrucción de llamada CALL de estos procesadores. La instrucción

CALL f


es equivalente a

PUSH l
JUMP f
l:

Donde l es una etiqueta, un indicador de dirección de código en ensamblador y JUMP una instrucción de salto. Esa es la continuación. La dirección de código l. Eso significa que podríamos llamar a la función doble en modo CPS de la siguiente forma.

PUSH x
PUSH k
JUMP doble

Las dos últimas instrucciones son el equivalente al CALL, pero diciendo que queremos que regrese, no a la instrucción inmediatamente posterior al salto, sino a la que sea k.


De esta manera se pueden mezclar funciones usuales con funciones en CPS. De hecho, las funciones usuales ya están en CPS sólo que "disfrazadas" por el uso de CALL.

Nota: Existe un pequeño detalle extra que es el uso exhaustivo que hace la transformación a CPS de las clausuras léxicas. Las funciones usuales guardan su entorno en la pila por lo que se destruye al salir de la función. Existen técnicas para evitar esto: los upvalues de LUA o en análisis de escape de la función para crear los entornos en el montículo en vez de la pila. 

jueves, 15 de septiembre de 2011

Incompatibilidades sintácticas

Los operadores prefijos e infijos no pueden ser a la vez identificadores

Si ese fuera el caso, tendríamos la siguiente ambigüedad.

a b c //=> (a) b (c) si b fuera infijo
a b c //=> a (b c) si b fuera prefijo

Una solución a esto es restringir o bien los operadores infijos o bien los prefijos para que no puedan ser identificadores.
 
Las palabras clave de las sentencias no pueden ser identificadores

Parece una perogrullada, pero hay una razón para que sea así.

return + a //=> return (+a) si es palabra clave
return + a //=> (return) + (a) si es identificador

Una solución a esto es reservar ciertos identificadores como palabras clave. Otra solución es marcar de alguna manera los identificadores que queramos que sean introductores de sentencias.
 
La tensión entre la llamada por yuxtaposición y los operadores como identificadores

No pueden compartir la misma categoría sintáctica (identificadores) ya que su asociatividad es distinta.

a b c //=> (a b) c  si es llamada por yuxtaposición (currificación)
a b c //=> a (b c)  si b es un operador prefijo
a b c //=> (a) b (c) si b es un operadores infijo

Una solución es no tener llamada por yuxtaposición. Otra solución es restringir los operadores a símbolos.

+ - c //=> + (- c)  OK. No se confunde. Si son símbolos, son prefijos.

Un operador no puede ser infijo, prefijo y postfijo a la vez

Si ese fuera el caso, tenemos la siguiente ambigüedad. Incluso con una categoría sintáctica distinta.

a + + b //=> (a +) + b  si el primer más es tomado como postfijo
a + + b //=> a + (+ b)  si el segundo más es tomado como prefijo

Llamada por yuxtaposición y tuplas son incompatibles con la sintaxis usual de llamada

Esto sólo es relevante si la semántica es distinta. Si la semántica es la misma, la ambigüedad confluye en un mismo significado.

f(a,b,c) //=> La función f aplicada por yuxtaposición a un argumento que es la tupla (a,b,c)
f(a,b,c) //=> La función f aplicada a tres argumentos que son a, b y c

Una solución a esto es usar otra sintaxis para las tuplas, por ejemplo [a,b,c].

Tuplas y parentización

Es una ambigüedad muy usual.

(1) //=> El número uno
(1) //=> Una tupla con un elemento que es el número uno

Algunos lenguajes como Python solucionan esto añadiendo una coma extra en el caso de ser una tupla y no una parentización.

(1,) //=> Una tupla en Python

viernes, 9 de septiembre de 2011

¿Qué es una clausura léxica?

Expresiones abiertas

Antes de ver qué es una clausura léxica debemos saber qué es una clausura y antes de saber qué es una clausura debemos saber qué es lo que cierra la clausura. Lo que la clausura cierra son expresiones abiertas. Ahora bien, ¿qué es una expresión abierta? Para responder a esta pregunta necesitamos saber antes qué es una variable ligada y una variable libre.

La forma más sencilla de explicarlo es mediante un mínimo ejemplo. Definamos dos funciones.

f= λx.x+3
g= λx.5x

Aunque hemos usado la misma variable “x” en ambas funciones, cada “x” tiene un significado distinto en cada función. No hay más que aplicar las funciones.

f(4) = 4+3
g(7) = 5·7

La “x” de la función “f” vale 4 y la “x” de la función “g” vale 7. Esto es así porque estas “x” son variables ligadas. La “x” de “x+3” está ligada a la “λx” de “f” y la “x” de “5x” está ligada a la “ λx” de “g”.

Otro ejemplo:

h = λx. x+y

En este caso la “x” de “x+y” está ligada a la “λx” de “h”. Por otra parte la “y” no está ligada, es una variable libre.

En “a+b” tanto “a” como “b” son variables libres y en “c+c” la “c” es una variable libre que aparece dos veces.

Se llaman expresiones cerradas a las que no tienen variables libres y se llaman expresiones abiertas a las que tienen variables libres.

“5+3” es una expresión cerrada.
“λx.2x” es una expresión cerrada.
“λx.x+x” es una expresión cerrada.
“λxy.2x+5y+3” es una expresión cerrada.
“a” es una expresión abierta (la variable “a” es libre).
“λy.y+x” es una expresión abierta (la variable “x” es libre).
“1+z” es una expresión abierta (la variable “z” es libre).

Clausuras

Realizar una clausura consiste en ligar las variables libres dándoles algún valor. Una clausura podría ser la clausura cero en la cual todas las variables libres toman el valor cero.

En esta clausura la expresión “3+x” es abierta, pero se cierra a “3+0”. Igualmente, la expresión “f= λx.x+y” que tiene de variable libre a la “y”, se cierra a “f= λx.x+0”.

Pero podríamos tener otro tipo de clausura. Por ejemplo, la que se usa en matemáticas que llamaremos aquí la clausura convencional. En esta clausura las variables libres toman el valor que hayan definido por nosotros los matemáticos.

La expresión “cos(3)” es abierta porque “cos” es una variable libre. Ahora bien, todo el mundo sabe que “cos” es el coseno por lo que cerramos la expresión ligando la variable “cos” con la función coseno.
En algunos casos la clausura convencional no nos sirve. Imaginemos que nos encontramos con la expresión “K(5)”. Leyendo las bibliotecas de funciones matemáticas nos damos cuenta que podría ser la función de Bessel o la función de Sturve o la integral elíptica de primer orden o varias cosas más.

La forma de resolverlo es haciendo una clausura global. En esta clausura ligamos explícitamente las variables a su significado siempre que no se diga lo contrario. Es lo que se suele hacer cuando definimos una función. El decir “f=λx.2x” significa que “f” es la función doble y, si más adelante tenemos “f(2)”, recordamos el significado de “f” que le dimos. Este tipo de clausura global es la que se usa en lenguajes de programación como el C.

Por cierto, cuando digo “f=λx.2x” no sólo estoy ligando “f” para la clausura global, también estoy ligando “x” en la expresión “2x”. Esta es la clausura local. En este punto, el lector con conocimiento de informática habrá identificado las clausuras con los entornos. Realmente la clausura es el entorno (el ligado de variables) más la expresión que se cierra.

La clausura léxica

El adjetivo “léxico” significa “relativo a las palabras de un lenguaje”. Es decir, que haremos la clausura según nos digan las palabras del lenguaje.


En los lenguajes de ordenador (y en las matemáticas) las expresiones se anidan unas dentro de otras. Así, la expresión “1+2” está dentro de la expresión “7+g(1+2)”. Esto da lugar a una estructura del propio lenguaje. La clausura léxica es la que respeta esa estructura.

{
      define x=5 //Definimos “x”
      define g(y)=x+y //Definimos “g”. La “x” es libre aquí
}

En este programa, la expresión “define g(y)=x+y” está dentro de la estructura superior “{ define x=5; define g(y)=x+y }”. En muchos lenguajes de programación las llaves {} se usan para mostrar la estructura del programa.

Cuando se define la función “g(y)”, la variable “y” está ligada, pero la “x” no. Sin embargo, siguiendo la estructura del programa, el valor de “x” está definido un poco más arriba. Esto quiere decir que la clausura léxica liga la variable “x” (dentro de la función de “g”) a la “x” que está definida fuera de esa definición, pero estructuralmente por encima de ella.

Podría uno pensar que la clausura léxica es un descontrol porque liga las variables como le da la gana. No es así. En este ejemplo, la clausura léxica no liga la “y” y un compilador daría un error.

{
      define g(y)=y+7
      define f(x)=x+y
}

No hay ligazón porque la expresión “define g(y)=y+7” no define la “y”, define la “g”. Además, la “y” de “g(y)” ya está ligada a la de “y+7”.

El siguiente diagrama muestra cómo, al realizar la clausura léxica, se van buscando estructura arriba las ligazones de variables.



La clausura léxica como valor

Una de las claves de la clausura léxica es que se son un valor. El ejemplo típico es el siguiente:

{
      define f(x)=λy.x+y;
      define a=f(2);
      define b=f(5);
      print a(4); //Imprime 6
      print b(4); //Imprime 9
}

En este ejemplo la expresión “λy.x+y” es una expresión abierta y la clausura léxica de la misma nos dice que hemos de ligar la “x” a la de la “f(x)”. Sin embargo esta “x” es el argumento de una función así que el resultado de “f(2)” es la clausura léxica de la expresión “λy.x+y” ligando la “x” al “2”. Esto es un valor como la cadena “hola”, sólo que es algo más largo de describir.

De la misma forma “b” se define a la clausura léxica de la expresión “λy.x+y” ligando “x” al “5”. Dado que la expresión “λy.x+y” es la función que toma un “y” y devuelve “x+y”, podemos usar esa función. Es lo que se hace en los “print”.

En el primer “print” usamos la variable “a” cuyo valor era “ λy.x+y” ligando la “x” al “2”. Al aplicar “a(4)” le damos un valor a “y” así que el resultado es “x+4” ligando la “x” al “2” que a su vez es “6”. Lo que se imprime. El mismo razonamiento obtenemos para “b(4)”.

La clausura léxica y la asignación

Existe un pequeño detalle final y consiste en distinguir ligazón y asignación. Realmente, realmente al hacer “f(2)” en este último ejemplo no ligamos la “x” de “λy.x+y” a “2”. Lo que hacemos es ligar esa “x” a la de “f(x)” y, luego, al hacer “f(2)” asignamos la “x” de “f(x)” a “2”. El siguiente diagrama lo muestra.


Esta distinción es importante porque en los lenguajes imperativos se permite reasignar. Es lo que ocurre en este código.

{
      define x=5;
      define f(y)=x+y;
      print f(3); //Imprime 8
      x=7;        //Reasignación de x
      print f(3); //Imprime 10
}

Cuando el lenguaje es funcional estas distinciones no son importantes porque no se puede reasignar.

viernes, 29 de abril de 2011

Transformación CPS

Hace algún tiempo hablamos de las continuaciones. Además, dimos una semántica operacional para ellas. Ahora vamos a describir cómo se transforma un programa para que use continuaciones en todas sus llamadas a función. Esto es el estilo de paso de continuaciones (CPS) que permite eliminar la necesidad de usar una pila de llamadas. Es decir, la cadena dinámica.

El estilo de paso de continuaciones (CPS)

Para empezar, veamos cómo es eso del CPS. Supongamos la siguiente expresión.
[$$ f(g(a)) $$]
Lo primero que calculamos es el valor de [$a$]. Luego, el valor de [$g(a)$] y, finalmente, el valor de [$f(g(a))$]. El estilo CPS añade un argumento adicional a cada función que es la continuación, a la que le pasaremos el resultado cuando la función termine.

¿A dónde pasábamos el resultado de [$g(a)$]? A [$f$]. Así que, al hacer [$g$] CPS, la expresión de arriba queda
[$$ g(f, a) $$]
y se leería algo así como "calcula [$g(a)$] y pásale el resultado a [$f$]. El problema es que también tenemos que hacer CPS a [$f$] por lo que ahora [$f$] no toma un argumento, toma dos: la continuación y su argumento propiamente dicho. La solución es añadir una función anónima. La expresión de arriba con tanto [$g$] como [$f$] puestas en CPS sería así:
[$$g(\lambda x.f(k,x), a)$$]
El detalle es que [$k$] es la continuación de la expresión, que no sabemos cuál es porque no he dicho qué vamos a hacer después de calcular [$f(g(a))$]. Para destacar claramente que [$k$] es la continuación, que [$f(g(a))$] es la expresión original y que [$g(\lambda x.f(k,x), a)$] es la expresión transformada a CPS, vamos a usar la siguiente notación:
[$$ T\ [\ f(g(a))\mid k\ ]\ = g(\lambda x.f(k,x), a) $$]

La transformación de Plotkin

Bastará conocer las reglas de la transformación [$ T\ [\ ]$] para poder realizarla. Esto es a lo que se dedicó Plotkin en 1975 en el artículo "Call-by-name, call-by-value and the [$\lambda$]-calculus", aunque ha llovido mucho y la versión que presento aquí es bastante distinta a la original.

Lo primero que necesitamos es definir el lenguaje que vamos a transformar. Vamos a usar una variante del lambda cálculo sin currificar. Es muy sencilla: un valor es o bien una variable o bien una función anónima.
[$$ v::= x \mid \lambda (x_1,...,x_n).e $$]
Una expresión es o bien un valor o bien una aplicación (llamo [$s$] a las aplicaciones).
[$$ e::= v \mid s $$]
También llamaremos, [$k$] a una expresión. Una aplicación no es más que una llamada a función.
[$$ s::= e_0 (e_1,...,e_n) $$]
Para transformar un programa escrito con esta sintaxis hemos de dar la transformación para cada una de estas construcciones sintácticas. Si lo que tengo un valor, ya está calculado, sólo he de pasarlo a la continuación. ¡Pero ojo! Si es una función he de transformarla antes en CPS. De esto se va a encargar la transformación auxiliar [$ M[\ ]$].
  • Regla P-TV: [$ T\ [\ v\mid k\ ]= k(M[\ v\ ])$]
La transformación auxiliar es bien sencilla: si no es una función no hace nada, si es una función le añade el argumento de la continuación y transforma su cuerpo.
  • Regla P-MX:  [$ M\ [\ x\ ]=x $]
  • Regla P-ML: [$ M\ [\ \lambda(x_1,...,x_n).e\ ]=\lambda(x,x_1,...,x_n).T[\ e\mid x\ ] $]
Finalmente, si lo que tenemos es una aplicación, vamos calculando su operador y argumentos hasta que, al final, podamos llamar a la función.
  • Regla P-TS: [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]=T[\ e_0 \mid \lambda x_0.T[\ e_1 \mid \lambda x_1.\ ...T[\ e_n \mid \lambda x_n. x_0(k, x_1,...,x_n)\ ]\ ...\ ]\ ] $]
Va a ser muy habitual encontrarnos con transformaciones anidadas del tipo [$...T[\ e\mid\lambda x.T[...$] ya que representan una secuencia de operaciones. El resultado de [$e$] se pasa a la continuación que empieza por [$\lambda x...$] lo que hace guardar el resultado de [$e$] en [$x$] y proseguir con la ejecución de la secuencia. Para facilitar la comprensión de una expresión en CPS vamos a poner como subíndice de una variable la expresión cuyo valor guarda.

Bien, ahora que tenemos las cuatro reglas de Plotkin vamos a probar a ver cómo transforma a [$f(g(a))$]. Vamos a hacerlo paso a paso.
  1. Inicial: [$T[\ f(g(a)) \mid k\ ]$]
  2. Regla P-TS: [$ T[\ f \mid \lambda x_f.T[\ g(a) \mid \lambda x_{g(a)}. x_f(k, x_{g(a)})\ ]\ ] $]
  3. Regla P-TV: [$ (\lambda x_f.T[\ g(a) \mid \lambda x_{g(a)}. x_f(k, x_{g(a)})\ ]\ )(M[\ f\ ]) $]
  4. Regla P-MV: [$ (\lambda x_f.T[\ g(a) \mid \lambda x_{g(a)}  . x_f(k, x_{g(a)})\ ]\ )(f) $]  
  5. Regla P-TS: [$ (\lambda x_f.T[g \mid \lambda x_g.T[\ a \mid \lambda x_a. x_g( \lambda x_1. x_f(k, x_{g(a)}) , x_a)\ ]\ ]\ )(f) $]  
  6. Regla P-TV: [$ (\lambda x_f. (\lambda x_g.T[\ a \mid \lambda x_a. x_g( \lambda x_{g(a)}. x_f(k, x_{g(a)}) , x_a)\ ]\ )(M[\ g\ ]) )(f) $]
  7. Regla P-MV: [$ (\lambda x_f. (\lambda x_g.T[\ a \mid \lambda x_a. x_g( \lambda x_{g(a)}. x_f(k, x_{g(a)}) , x_a)\ ]\ )(g) )(f) $]
  8. Regla P-TV: [$ (\lambda x_f. (\lambda x_g.  (\lambda x_a. x_g( \lambda x_{g(a)}. x_f(k, x_{g(a)}) , x_a) )(M[\ a\ ]\ ) )(g) )(f) $]
  9. Regla P-MV: [$ (\lambda x_f. (\lambda x_g.  (\lambda x_a. x_g( \lambda x_{g(a)}. x_f(k, x_{g(a)}) , x_a) )(a) )(g) )(f) $]
 El resultado de esta expresión es sorprendentemente complejo. Además, parece que podríamos usar varias veces la regla beta para reducir un poco más la expresión.
  1. Del anterior: [$ (\lambda x_f. (\lambda x_g.  (\lambda x_a. x_g( \lambda x_{g(a)}. x_f(k, x_{g(a)}) , x_a) )(a) )(g) )(f) $]
  2. Regla beta: [$ (\lambda x_g.  (\lambda x_a. x_g( \lambda x_{g(a)}. f(k, x_{g(a)}) , x_a) )(a) )(g) $]
  3. Regla beta: [$ (\lambda x_a. g( \lambda x_{g(a)}. f(k, x_{g(a)}) , x_a) )(a) $]
  4. Regla beta: [$ g( \lambda x_{g(a)}. f(k, x_{g(a)}) , a) $]
Esto sí se parece a lo que teníamos inicialmente, el problema es que no podemos realizar reglas beta ya que estamos transformando código y ¿cómo sabemos qué lambdas son del código y qué lambdas son las introducidas por la transformación?

¡Ah!... Se me ocurre una idea. Podríamos etiquetarlas...

Nota: ¿Por qué no podemos realizar reglas beta en el código? Imaginemos la siguiente expresión [$(\lambda x.3)(f(x))$]. Si [$f(x)$] tiene efectos secundarios, al realizar la regla beta obtendríamos [$3$] y habríamos perdido los efectos secundarios.

 La tranformación de alto nivel de Appel

Esta fue la idea que se le ocurrió a Appel en 1992 para su libro "Compiling with Continuations", pero no es tan sencilla. En primer lugar estaríamos tentados a etiquetar (con una rayita encima) las lambda que fueran introducidas por la transformación a CPS. Empecemos por la regla que las introduce: la P-TS que renombraremos a A-TS.

  • Regla A-TS: [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]=$][$T[\ e_0 \mid \bar\lambda x_0.T[\ e_1 \mid \bar\lambda x_1.\ ...T[\ e_n \mid \bar\lambda x_n. x_0(k, x_1,...,x_n)\ ]\ ...\ ]\ ] $]
El problema es que las [$\bar\lambda$] no son código, son funciones sobre el código (de ahí lo de alto nivel). Si dejáramos las cosas así obtendríamos
[$$ T[\ f(a)(b)\ ]=f(\bar\lambda x_{f(a)}.x_{f(a)}(k,b), a) $$]
Y eso no es código. No es código porque contiene un [$\bar\lambda$] que es una función sobre el código. No está en la sintaxis que pusimos al inicio.

La solución se obtiene forzando que las continuaciones se especifiquen siempre de forma indirecta, como una función sobre el código. En ese aspecto, cambiamos la regla P-ML de la siguiente forma (la nueva regla es la A-ML):
  • Regla P-ML: [$ M\ [\ \lambda(x_1,...,x_n).e\ ]=\lambda(x,x_1,...,x_n).T[\ e\mid x\ ] $]  
  • Regla A-ML: [$ M\ [\ \lambda(x_1,...,x_n).e\ ]=\lambda(x,x_1,...,x_n).T[\ e\mid \bar\lambda x_0.x x_0\ ] $]  
El truco está en que [$\bar\lambda x_0.x x_0$] convierte el código [$x$] en una función que toma código [$x_0$] y devuelve código [$x x_0$]. Como [$x$] va a ser la continuación, el efecto es pasarle [$x_0$] a la continuación. Justo lo que teníamos antes pero, esta vez, el [$\bar\lambda$] significa que estoy trabajando con funciones sobre código y no sobre código. Podré usar la regla beta para simplificarlas. Lo que buscábamos a la hora de etiquetar.

También hay que trastear la regla A-TS porque [$k$] no es código.
  • Regla P-TS: [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]= $][$ T[\ e_0 \mid \lambda x_0.T[\ e_1 \mid \lambda x_1.\ ...T[\ e_n \mid \lambda x_n. x_0(k, x_1,...,x_n)\ ]\ ...\ ]\ ] $] 
  •  Regla A-TS antigua (etiqueto lambdas): [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]= $][$ T[\ e_0 \mid \bar\lambda x_0.T[\ e_1 \mid \bar\lambda x_1.\ ...T[\ e_n \mid \bar \lambda x_n. x_0(k, x_1,...,x_n)\ ]\ ...\ ]\ ] $] 
  • Regla A-TS nueva (arreglo [$k$]): [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]= $][$ T[\ e_0 \mid \bar\lambda x_0.T[\ e_1 \mid \bar\lambda x_1.\ ...T[\ e_n \mid \bar \lambda x_n. x_0(\lambda x.k x, x_1,...,x_n)\ ]\ ...\ ]\ ] $]
El truco de [$\lambda x.kx$] es que [$k$] es una función de código que tomará el código [$x$] y generará otro código que permanece dentro del cuerpo de [$\lambda x$]. El resultado es, por tanto, código. Para dejar claro qué variables son de código y qué variables son funciones sobre el código, también etiquetaremos estas últimas que incluirán ahora todas las continuaciones también.

Entonces, las reglas de transformación de alto nivel definitivas son las siguientes:

  • Regla A-MX:  [$ M\ [\ x\ ]=x $]
  • Regla A-ML: [$ M\ [\ \lambda(x_1,...,x_n).e\ ]=\lambda(x,x_1,...,x_n).T[\ e\mid   \bar\lambda x_0.x x_0  \ ] $]
  • Regla A-TV: [$ T\ [\ v\mid \bar k\ ]= k(M[\ v\ ])$]
  • Regla A-TS: [$ T\ [\ e_0(e_1,...,e_n) \mid \bar k\ ]=$][$T[\ e_0 \mid \bar\lambda x_0.T[\ e_1 \mid \bar\lambda x_1.\ ...T[\ e_n \mid \bar\lambda x_n. x_0(\lambda x.\bar k x, x_1,...,x_n)\ ]\ ...\ ]\ ] $]
  •  Regla beta: [$ (\bar\lambda (x_1,...x_n). e) (v_1,...,v_n) = [x_1 \rightarrow v_1,...,  x_n \rightarrow v_n  ]e $]
Mejoran mucho el resultado de la transformación a CPS. El único inconveniente es que debemos trabajar con funciones sobre código (de alto nivel). Así que, si [$k$] sin etiquetar es código, no puedo escribir
[$$ T[\ f(a)\mid k\ ]$$]
Tengo que escribir
[$$ T[\ f(a)\mid \bar\lambda x.k x\ ]$$]
y el resultado de esto no es todo lo perfecto que quisiéramos:
[$$ f( \lambda x.k x, a) $$]
Se me ocurre otra idea... ¿Y no podemos mezclar las reglas de Plotkin y de Appel para usar código o funciones sobre código cuando queramos?

La tranformación CPS híbrida

Separemos entonces [$T\ [\ ]$] en dos: una sin etiqueta y otra con etiqueta [$\bar T\ [\ ]$]. La versión sin etiqueta requiere que la continuación sea código [$T\ [\ e\mid k\ ]$] y la etiquetada requiere que sea una función sobre código [$\bar T\ [\ e\mid \bar k\ ]$]. La mezcla es sencilla. 
  • Regla H-MX:  [$ M\ [\ x\ ]=x $]
  • Regla H-ML: [$ M\ [\ \lambda(x_1,...,x_n).e\ ]=\lambda(x,x_1,...,x_n).T[\ e\mid x \ ] $]
  • Regla H-TV: [$ T\ [\ v\mid k\ ]= k(M[\ v\ ])$]
  • Regla H-TS: [$ T\ [\ e_0(e_1,...,e_n) \mid k\ ]=$][$\bar T[\ e_0 \mid \bar\lambda x_0. \bar T[\ e_1 \mid \bar\lambda x_1.\ ... \bar T[\ e_n \mid \bar\lambda x_n. x_0(k, x_1,...,x_n)\ ]\ ...\ ]\ ] $]  
  • Regla H-FV: [$ \bar T\ [\ v\mid \bar k\ ]= \bar k(M[\ v\ ])$]
  • Regla H-FS: [$ \bar T\ [\ e_0(e_1,...,e_n) \mid \bar k\ ]=$] [$\bar T[\ e_0 \mid \bar\lambda x_0. \bar T[\ e_1 \mid \bar\lambda x_1.\ ... \bar T[\ e_n \mid \bar\lambda x_n. x_0(\lambda x.\bar k x, x_1,...,x_n)\ ]\ ...\ ]\ ] $]
  • Regla beta: [$ (\bar\lambda (x_1,...x_n). e) (v_1,...,v_n) = [x_1 \rightarrow v_1,...,  x_n \rightarrow v_n  ]e $]


Con estas reglas sí que conseguimos el objetivo deseado.
[$$ T\ [\ f(a)\mid k\ ]\ = f(k,a)$$]
[$$ T\ [\ f(g(a))\mid k\ ]\ = g(\lambda x.f(k,x), a) $$]
El único problemilla que tenemos es que trabajamos como en dos fases. En la primera fase quitamos las [$T$] (y [$\bar T$]) de en medio y en la segunda fase quitamos las [$\bar \lambda$] con la regla beta. Es posible hacerlo todo a la vez, pero aumenta lo suficiente la complejidad como para no explicarlo aquí (se basa en separar las [$e$] en [$v$] y en [$s$] y tratar cada caso con una regla distinta, específica).

Transformación CPS para lenguajes reales

Para finalizar vamos a apuntar brevemente cómo son las reglas de transformación CPS para un lenguaje de programación real, con asignación, selección, secuencia y demás primitivas.
  • AsignaciónT: [$ T\ [\ \mathrm{set}\ x\ e \mid k\ ]=\bar T[ e \mid \bar\lambda x_0. k (  \mathrm{set}\   x\ e)\ ] $]
  • AsignaciónF: [$ \bar T\ [\ \mathrm{set}\  x\ e \mid \bar k\ ]=\bar T[ e \mid \bar\lambda x_0. \bar k (  \mathrm{set}\   x\ e)\ ] $]
  • SelecciónT: [$ T\ [\ \mathrm{if}\ e_1\ e_2\ e_3 \mid k\ ]=\bar T[\ e_1 \mid \bar\lambda x_1. (  \mathrm{if}\ x_1\ T[\ e_2 \mid k\ ]\ T[\ e_3 \mid k\ ]\ ] $]
  • SelecciónF: [$ \bar T\ [\   \mathrm{if}\ e_1\ e_2\ e_3 \mid \bar k\ ]=\bar T[\ e_1 \mid \bar\lambda x_1. (  \mathrm{if}\ x_1\ \bar T[\ e_2 \mid \bar k\ ]\ \bar T[\ e_3 \mid \bar k\ ]\ ] $]
  • SecuenciaT: [$ T\ [\ e_1 ; e_2 \mid k \ ]= \bar T[\ e_1 \mid \bar\lambda x.T[\ e_2 \mid k\ ]\ ] $]
  • SecuenciaF: [$ \bar T\ [\ e_1 ; e_2 \mid \bar k \ ]= \bar T[\ e_1 \mid \bar\lambda x. \bar T[\ e_2 \mid \bar k\ ]\ ] $]
La selección duplica el código que hay en la continuación porque lo usa en dos sitios. Esto puede dar lugar a un código final más grande de lo deseable. La solución es postergar a tiempo de ejecución el paso de la continuación de la siguiente forma:
  • SelecciónT2: [$ T\ [\ \mathrm{if}\ e_1\ e_2\ e_3 \mid k\ ]=(\lambda x. \bar T[\ e_1 \mid \bar\lambda x_1. (  \mathrm{if}\ x_1\ T[\ e_2 \mid k\ ]\ T[\ e_3 \mid x\ ]\ ])(k) $]   
  • SelecciónF2: [$ \bar T\ [\   \mathrm{if}\ e_1\ e_2\ e_3 \mid \bar k\ ]=(\lambda x. \bar T[\ e_1 \mid \bar\lambda x_1. (  \mathrm{if}\ x_1\ \bar T[\ e_2 \mid \bar x\ ]\ \bar T[\ e_3 \mid \bar k\ ]\ ])(\lambda x_0.\bar k x_0) $]  
La clásica función call/cc se implementa así.
  • CallCCT: [$ T\ [\ \mathrm{calcc}\ | k\ ] = k (\lambda(x_0,x_1).x_1(x_0,\lambda(x_2,x_3).x_0(x_3))) $]
  • CallCCF: [$ \bar T\ [\ \mathrm{calcc} \ | \bar k\ ] =  \bar k (\lambda(x_0,x_1).x_1(x_0,\lambda(x_2,x_3).x_0(x_3))) $]
Los let se pueden implementar como lambdas aplicados inmediatamente, pero los letrec, no. Asumimos que los letrec sólo pueden vincular valores. Entonces:
  • LetRecT: [$ T\ [\ \mathrm{letrec}\ x_1=v_1,...,x_n=v_n\ \mathrm{in}\ e \mid k\ ]=$][$\mathrm{letrec}\ x_1=M[\ v_1\ ],...,x_n=M[\ v_n\ ]\ \mathrm{in}\ T[\ e \mid k\ ] $]
  • LetRecF: [$ \bar T\ [\ \mathrm{letrec}\ x_1=v_1,...,x_n=v_n\ \mathrm{in}\ e \mid \bar k\ ]=$][$\mathrm{letrec}\ x_1=M[\ v_1\ ],...,x_n=M[\ v_n\ ]\ \mathrm{in}\ \bar T[\ e \mid \bar k\ ] $]
Referencias

Matt Might - How to compile with continuations
Mark Feeley - The 90 minute Scheme to C compiler