Lenguajes de Programación
Transcripción
Lenguajes de Programación
Lenguajes de Programación Adrián Pérez Alonso 21 de junio de 2013 1. Paradigma imperativo. Máquina de Turing Una máquina de Turing es un dispositivo teórico que manipula sı́mbolos en una cinta infinita de acuerdo a una tabla de reglas. A pesar de su simplicidad, puede adaptarse para simular la lógica de cualquier algoritmo de computación y es especialmente útil para explicar el funcionamiento dentro de una CPU, y en especial el modelo de cálculo del paradigma de programación imperativo. [6] La máquina consta de una cabeza lectora que se mueve a derecha e izquierda y cuando se mueve puede modificar el sı́mbolo de la cinta al que apunta. La máquina de Turing se define mediante la siguiente séptupla: [1](cáp. 8.2.2) M = (Q, Σ, Γ, δ, q0 , B, F ) Q Es el conjunto finito de estados de la unidad de control. Σ Es el conjunto finito de sı́mbolos de entrada Γ Es el conjunto completo de sı́mbolos de cinta /Σ ⊂ Γ δ Es la función de transición. Los argumentos de δ(q, X) son un estado q y un sı́mbolo de cinta X. El valor de δ(q, X), si está definido, es (p,Y,D) donde p es el siguiente estado de Q, Y es el sı́mbolo de Γ y D es la dirección que puede ser derecha o izquierda. q0 Es el estado inicial. q0 ∈ Q B Es el sı́mbolo espacio en blanco. B ∈ Γ F Es el conjunto finales o de aceptación. F ⊂ Q Ejemplo: Diseñar una máquina de Turing que acepte el lenguaje {0n 1n |n ≥ 1} M = ({q0 , q1 , q2 , q3 , q4 }, {0, 1}, {0, 1, X, Y, B}, δ, q0 , B, {q4 }) donde δ se especifica en la siguiente tabla: [1](cáp. 8.2) 0 1 X Y B Estado q0 (q1 , X, D) (q3 , Y, D) q1 (q1 , 0, D) (q2 , Y, I) (q1 , Y, D) q2 (q2 , 0, I) (q0 , X, D) (q2 , Y, I) q3 (q3 , Y, D) (q4 , B, D) q4 La secuencia de movimientos para la entrada 0011 será la siguiente (aceptada por la MT): q0 0011 ` Xq1 011 ` X0q1 11 ` Xq2 0Y 1 ` q2 X0Y 1 ` Xq0 0Y 1 ` XXq1 Y 1 ` XXY q1 1 ` XXq2 Y Y ` Xq2 XY Y ` XXq0 Y Y ` XXY q3 Y ` XXY Y q3 B ` XXY Y Bq4 B Veamos cual es la secuencia de movimientos para la entrada no aceptada 001: q0 001 ` Xq1 01 ` X0q1 1 ` Xq2 0Y ` q2 X0Y ` Xq0 0Y ` XXq1 Y ` XXY q1 B 1 1.1. Recursividad El término ‘recursivo’ se utiliza en matemáticas como sinónimo del término ‘decidible’, no tiene que ver con el término de recursividad en programación. Definición 1.1. Sea la función entera f calculada por M. Si M está definido en toda la entrada para f decimos q f es totalmente recursiva. La MT de una función totalmente recursiva siempre termina en el estado final y genera una respuesta. [7] Definición 1.2. Una función parcialmente recursiva será aquella cuya máquina de Turing pare, pero no sabemos si lo hará en el estado final o no. [7] Si pensamos en el lenguaje de una función totalmente recursiva como en un ‘problema’, entonces se dice que el problema es decidible si es un lenguaje recursivo e indecidible si es un lenguaje no recursivo. Es decir, si no sabemos si la MT se para o no se para, decimos que el problema es no decidible y por lo tanto no sabemos si se puede resolver (si es resoluble) o si no se puede resolver (no resoluble). Tambien existen los problemas intratables, que son problemas recursivos y resolubles de los que no se puede obtener un resultado final. Esto es debido a que su complejidad es exponencial y por lo tanto, siempre existirá alguna posibilidad de que, aun con capacidades de computación mucho mas elevadas a las actuales, no seamos capaces de hacer frente a cálculos tan complejos. Un ejemplo de este tipo de problemas es el problema del viajante para un número elevado de ciudades a visitar. 1.2. El problema de la parada Despues de esta introducción, nos damos cuenta de que no todos los problemas se pueden resolver, y que la resolución de los problemas dependerá de que la MT acepte o no el lenguaje y si esta se para o no se para. Este problema se conoce como el problema de la parada (Halting problem). Se podrı́a definir H(M) (halting problem para la MT M) como el conjunto de entradas, tales que M se detiene para la entrada dada, independientemente de si M acepta o no la entrada.[1](cáp. 9.2.4) 1.2.1. Hipótesis de Church Todas las propuestas serias de modelos de computación calculan las mismas funciones o reconocen los mismos lenguajes. La hipótesis (no demostrada) de Church dice: [1](cáp. 8.2.1) Definición 1.3. Cualquier forma general de computación no permite calcular sólo las funciones recursivas parciales (que es lo que pueden calcular las computadoras actuales y las máquinas de Turing) 2. Paradigma funcional. λ-calculus Los términos de λ-calculus, o λ-términos, serán usados para definir funciones. La función x 7−→ x + 1 será escrita en lisp de la siguente forma (lambda(x)0 (+x1)). [2] (cáp. 3) Para expresiones aritméticas, existen (por ejemplo) los operadores (+, *, etc), variables y constantes. La estructura de estas expresiones viene del aspecto funcional de los sı́mbolos. Cada sı́mbolo requiere un número determinado de argumentos llamado aridad. 2.1. Σ-notación Definición 2.1. Sea Σ un alfabeto, definimos Σ-términos finitos (TΣ ) como el subconjunto más pequeño de Σ. [2](cáp. 2.2.1) Si f ∈ Σ y ar(f)=n, f es de aridad n, también se dice que f es n-ario. Por ejemplo: ‘not’ es un operador unario, ‘suma’ es un operador binaro, etc. Si c ∈ Σ, ar(c)=0, entonces c ∈ E. Denotamos a c sı́mbolo constante si la aridad es igual a 0 2 Si f ∈ Σ, ar(f ) = n ≥ 1, y si M1 , ..., Mn ∈ E, entonces f M1 , ..., Mn ∈ E. Denotamos a f sı́mbolo funcional si la aridad es mayor o igual a 1. Definimos TΣ (x) como el álgebra de Σ-términos finitos como variables en el conjunto x. De una forma mas intuitiva, podrı́amos decir que TΣ (x) es el conjunto de todas las posibles combinaciones de objetos de un lenguaje. [7] Ejemplo: (1*2)*3. (Numeros naturales y simbolo ‘∗0 ) Σ = {N+, ∗} X = {x, y} * * 1 3 2 (1) ∈ TΣ (x) Ejemplo: f1 M ≡ {ε, 1, 2, 3, 2,1, 2,2, 3,1, 3,2} ≡ a1 g2 a1 h3 a2 a1 (2) a2 Sea M ∈ TΣ (x), donde M es un árbol, definimos la siguiente notación[7]: Θ(M ) se define como el conjunto de nodos del árbol M. M(u) donde u ∈ Θ(M ). M(u) se define como la etiqueta del nodo ‘u’. Algunos ejemplos para el árbol dado anteriormente serı́an: M (ε) = f1 M (2,1) = a1 M (3) = h3 M/u donde u ∈ Θ(M ). M/u es el subtérmino (es decir, el subárbol) que ‘cuelga’ del nodo ‘u’. Algunos ejemplos para el árbol dado anteriormente serı́an: M (ε) = M (3) g M/2 = (4) a1 2.2. a2 λ-notación λ-notación es un caso particular y mas especı́fico de Σ-notación. Un λ-término se construye como un subconjunto de TΣ (x) donde: [2](cáp. 2.3.2) Sea x ∈ X una variable ⇒ x es un λ-término. Sea x ∈ X una variable y M un λ-término ⇒ λxM es un λ-término llamado abstracción (definición de la función). 3 Sean M,N λ-términos ⇒ MN es un λ-término llamado aplicación de M sobre N. Donde λ es el nombre de la función, x son los parámetros de la función, M es el cuerpo de la función y N son los argumentos de la función. Definición 2.2. Un redex es una aplicación cuyo término izquierdo es una abstracción (aplicación sobre una abstracción, (λxM )N ). Por ejemplo: con el redex I ≡ λf x.f (f x) (redex de nombre ‘I’ donde el parámetro de la función f (x) se aplica sobre si misma), IPQ contrae a P(PQ). Un término que no tiene un subtermino que es un redex es llamado normal (ver a continuación). El conjunto de todos los subtérminos de M que son redex se escribe Ored (M ). Entonces M es normal ⇔ Ored (M ) = ∅. El resultado de contraer el redex (λxM )N es el término M [x := N ]. Donde M [x := N ] es la sustitución de x por N aplicada a M. [2] (cáp. 3.3.4) En lenguaje coloquial un redex se podrı́a definir como un término que puede ser ‘simplificado’. Nota: Los λ-términos representan funciones o trozos de funciones. Pongamos por ejemplo la función (λx)(x + 1)(3) que tendrı́a un equivalente en lisp a (def un masuno (x)(+ x 1))(masuno 3) y un equivalente en C a masuno(x){return x + 1; } masuno(3); 2.3. β-reducción Definición 2.3. Denominamos β-reducción al proceso de derivación que sirve de base a la interpretación funcional.[7] Como hemos visto anteriormente, cualquier λ-término es una variable, una aplicación o una abstracción. El término β-reducción se define como una relación basada en la contracción de un redex. Se diferencian los siguientes tipos: [2] (cáp. 3.1.1) β-reducción inmediata (→β ) (en un solo paso) β-reducción (→∗β ) (en múltiples pasos) β-conversion (=β ) (inter-reducción) Cada una de estas definiciones es dada usando un sistema de inferencia que contiene la misma sentencia M B N , donde M y N son términos y B es un nuevo sı́mbolo que significa ‘se reduce a’. [2] (cáp. 3) Definición 2.4. Inferencia es el acto de derivar conclusiones lógicas a partir de premisas conocidas o asumiendo que es cierto [5] Definición 2.5. Sean M y N ∈ TΣ (x) decimos q M se β-reduce a N si existe una derivación que transforme M en N mediante la combinación de las siguientes reglas de inferencia: [7] Definición 2.6. β-reducción es una relacion binaria escrita M →β N si y solo si existe una derivación cerrada de la sentencia M B N del sistema de inferencia: [2] (cáp. 3.1.1) (red) : M B M0 (λ) : λxM B λxM 0 (λxP )Q B P [x := Q] M B M0 M B M0 (1) : (2) : M N B M 0N NM B NM0 4 El primero es el caso mas básico, donde se dice que un redex se reduce cambiando los valores de x por la aplicación (Q) en la abstracción (P). En los sigientes casos decimos que si un término M se reduce en otro término M’, un término mas complejo, también se reducirá. De este modo en la segunda regla de inferencia decimos como se reduce una abstracción, y en las dos reglas siguientes decimos como se reduce una aplicación tanto por la derecha, como por la izquierda. 2.4. 2.4.1. Propiedades fundamentales Confluencia Para la relación binaria → sobre E, donde x ∈ E. Se define: [2](cáp. 3.1.2) Definición 2.7. Confluente en x ⇔ ∀x1 , x2 ∈ E/ x →∗ x1 x →∗ x0 , ∃x0 ∈ E/ 1 ∗ 0 ∗ x → x2 x2 → x Definición 2.8. Localmente confluente en x ⇔ ∀x1 , x2 ∈ E/ x → x1 x →∗ x0 , ∃x0 ∈ E/ 1 ∗ 0 x → x2 x2 → x Definición 2.9. Fuertemente confluente en x ⇔ ∀x1 , x2 ∈ E/ x → x1 x → x0 , ∃x0 ∈ E/ 1 x → x2 x2 → x0 2.4.2. Normalización Definición 2.10. Si M es un λ-término normalizable, ⇒ ∃ una β-reducción M →∗ N , tal que N es normal. [2](cáp. 3.1.2) Es decir, un λ-término es normalizable si es interpretable hasta sus últimas consecuencias. De este modo un λ-término normal ha sido ‘simplificado’ al máximo. 2.4.3. Teorema Church-Rosser Definición 2.11. El teorema de Church-Rosser dice que la β-reducción →β es confluyente. Si M es normalizable, ∃N/N ≡ λ-término normal /M ↔∗ N [2](cáp. 3.1.2) Definición 2.12. El problema de la normalización de un λ-término no decidible. [2](cáp. 3.1.2) Un λ-término puede ser normalizable, y sin embargo, caer en una estrategia de normalización que no nos lo permita. Veámoslo en los siguientes ejemplos: [7] Ejemplo: 5 + 9 = 14 5 + 9 = 9 + 5 = 5 + 9 = ... Ejemplo: ¿Es siempre decidible la normalización de la siguiente expresión? (λxy)(λx.xx)(λx.xx) En este ejemplo podemos realizar la normalización por la izquierda de forma exitosa, donde el primer grupo de paréntesis (λxy) será la declaración de la función lambda con los parámetros x e y, el segundo paréntesis (λx.x x) será el cuerpo de la función, y el tercer paréntesis (λx.x x), serán los parámetros con los que se llama a esa función. Vemos que el parámetro ‘y’ no es usado (no aparece en el cuerpo de la función): (λxy)(λx.xx)(λx.xx) = (λx.λxλx) Donde (λx.λxλx) no es un redex y por lo tanto no es normalizable. 5 Esta normalización la podrı́amos expresar de una forma (seguramente) más simple, con un sı́mil a un lenguaje imperativo de programación 1 : lambda(x, y){return λx.x x; }; lambda(λx, xx); Podemos ver que el resultado de esta llamada a la función lambda será la devolución de λx. seguido dos veces del parámetro segundo (xx) , es decir, será (λx.λxλx). Si realizamos la normalización por la derecha, no obtendremos éxito. Para normalizar de este modo, debemos dejar de lado el término (λxy) y trabajar con (λx.xx)(λx.xx) de forma que λx será la declaración a nuestra función, xx será el cuerpo de la misma y (λx.xx) será el parámetro con los que se llama esa función: (λxy)(λx.xx)(λx.xx) = (λxy)(λx.xx)(λx.xx) Vemos que obtenemos el mismo resultado del que partı́amos, que es a su vez un redex, por lo que concluimos que no se puede realizar una normalización por la derecha. Esta normalización la podrı́amos expresar de una forma (seguramente) más simple, con un sı́mil a un lenguaje imperativo de programación 2 : lambda(x){return x x; }; lambda(λx.xx); De esta función obtendrı́amos el único parámetro de entrada duplicado, es decir (λx.xx)(λx.xx) al que deberı́amos adjuntar el elemento que eliminamos anteriormente (λxy), quedando finalmente la misma expresión que tenı́amos en origen, por lo que no la podemos normalizar: (λxy)(λx.xx)(λx.xx) 3. Paradigma lógico Definición 3.1. Un injerto es el complemento de la construcción de subárboles [7] En programación, el injerto de código se llama macro. En lenguaje C se hace mediante la directiva de preprocesado #define. Por ejemplo: sea P ≡ Kx (función constante de valor x), y M ≡ λxN (función x → N ). Entonces M [λ ← P ] ≡ λx(Kx) (función x → Kx, la funcion constante de valor x). Definición 3.2. Una sustitución es una lista de pares (variable, término). Sean x un conjunto de variables sobre Σ∗, σ es una sustitución: [7] σ ≡ X → T2 (x) Q = {X1 ← T1 , X2 ← T2 , ..., Xn ← Tn } 3.1. Unificación El paso de variables a constantes se le da el nombre de unificación o proceso de resolución Definición 3.3. Un unificador de dos expresiones (términos) T1 y T2 , es una sustitución σ que hace T1 σ = T2 σ [3] (cáp 7.2.2) Es decir, una unificación es una sustitución que aplicada sobre dos términos, los iguala. En este ejemplo deducimos que pepe es el hijo de luis: 1 Este 2 Este ejemplo no es totalmente exacto, pero es una ilustración para facilitar el entendimiento por parte del lector ejemplo no es totalmente exacto, pero es una ilustración para facilitar el entendimiento por parte del lector 6 padre(y,x) → hijo(x,y) padre(luis,pepe) hijo(pepe,luis) Ejemplo: Dados los siguientes términos lógicos: T1 = f (X, g(X, h(Y ))) T2 = f (Z, g(Z, Z)) Unificando según Q obtenemos el siguiente resultado: Θ = {X ← h(1), Z ← h(1), Y ← 1} T1 = f (h(1), g(h(1), h(1))) T1 = f (h(1), g(h(1), h(1))) Vemos que la unificación no es única, también podrı́amos aplicar lo siguiente: Θ = {X ← Z, Z ← h(Y )} Θ = {X ← Z, Z ← h(1), Y ← 1} Definición 3.4. Dado Σ como un conjunto de todos los unificadores de T1 y T2 . Existe una substitución µ ∈ Σ que tiene la propiedad µσ = σ para todo σ ∈ Σ y es llamada unificador mas general (mgu) de T1 y T2 [3] (cáp 7.2.2) 3.2. Algoritmo de Robinson El algoritmo de Robinson es el algoritmo de unificación mas usado en los intérpretes Prolog. Representa el sistema de ecuaciones como una pila y se opera con la ecuación mas superior a menos que esta no se pueda aplicar. [2] (cáp. 6.2.4) Ante la entrada de dos términos T1 y T2 obtendremos una salida de σ = mgu(T1 , T2 ) si es que este existe. En otro caso obtendremos fail. El siguiente pseudocódigo describe el método de unificación de Robinson [4](Algoritmo 6.3.1). Entrada: Dos términos lógicos T1 y T2 . Salida: El mgu(T1 ,T2 ), si existe; en otro caso fail. INICIO Θ := ∅ push (T1 = T2 , Pila ) MIENTRAS Pila 6= ∅ HACER ( X = Y ) := pop ( Pila ) CASO X ∈ / Y : sustituir (X , Y ) anadir (Θ, X ← Y ) Y ∈ / X : sustituir (Y , X ) anadir (Θ, Y ← X ) ( X←Y ) : NADA ( X←f ( X1 , ... , Xn )) y ( Y←f ( Y1 , ... , Yn )) : PARA i := n HASTA 1 PASO -1 HACER push ( Xi = Yi , Pila ) FIN PARA SINO : DEVOLVER fail FIN CASO FIN MIENTRAS DEVOLVER Θ FIN 7 Donde la función push(T1 = T2 , Pila) introduce la ecuación lógica T1 = T2 en la pila y la función pop(Pila) extrae la ecuación lógica X=Y de la pila. La función sustituir (X, Y ) sustituye la variable X por Y en la pila y en la sustitución Θ. Finalmente, la función anadir(Θ, σ) añade al unificador Θ (la variable que vamos a devolver en la finalización) la sustitución que realicemos en cada caso. Ejemplo: Podemos describir de esta forma la unificación de los términos, siguiendo la siguiente secuencia de configuraciones de la pila [4] (cáp. 6.3): T1 = f (X, g(X, h(Y ))) T2 = f (Z, g(Z, Z)) Θ≡{} ` T1 = T2 Θ≡{} Θ ≡ { X ← Z} Θ ≡ { X ← Z} X=Z Z=Z ` ` ` g(Z,Z) = g(X, h(Y) ) g(Z,Z) = g(X, h(Y) ) Z = h(Y) Θ ≡ { X ← Z} Θ ≡ { X ← Z ← h(Y), Z ← h(Y) } ` Z = h(Y) En este ejemplo podemos ver, en cada momento del algoritmo, el valor de Θ (que será al final el valor del mgu(T1 , T2 ) que devolvamos y que comienza siendo vacı́o) y el último elemento de la pila (el único elemento al que podemos tener acceso de la pila). Además vemos también el elemento que añadimos mediante el push(T1 = T2 , Pila). Un aspecto importante es el llamado test de ciclicidad que se expresa mediante X ∈ /YyY∈ / X. Debido al elevado costo de su aplicación, la mayorı́a de intérpretes Prolog lo eliminan, lo que puede dar lugar errores a la hora de la programación, como se ve en el siguiente ejemplo. [4](cáp 6.3) Interroguemos con la siguiente pregunta, suponiendo que tenemos implementado el término igual(X, X): : −igual(Y, f (Y )). Θ≡{} Θ ≡ { X ← Z} X=Y ` Y=f(Y) X=f(Y) Θ≡{} ` igual(X,X)=igual(Y,f(Y)) Observemos que en la ultima de las configuraciones Y aparece en f(Y) y parece que en unifican, pero en realidad no lo hacen. Si continuamos con el proceso ocurrirá que sustituiremos toda ocurrencia de Y por f(Y), y como consecuencia obtendremos que la unificación entra en un ciclo: X ← Y ← f (Y =← f (f (Y )) ← f (f (f (Y ))) ← ... ¿Es entonces imposible trabajar con términos cı́clicos? La respuesta es no. Es posible en algunos casos, en particular este ejemplo si es resoluble, pero por razones de eficiencia computacional se opta por una respuesta fail. 3.3. Algoritmo de resolución [7] La mayorı́a de los intérpretes Prolog están basados en el método de resolución lógica denominado SLD3 que se describe mediante el siguiente algoritmo: Entrada: Un programa lógico y una pregunta Q. Salida: QΘ, si Q es una instancia deducible a partir de P; en otro caso FAIL. 3 por Selecting a literal, using a Linear strategy and searching the space of possible deductions Depth-first. 8 INICIO Resolvente := { Q } MIENTRAS Resolvente 6= ∅ HACER A ∈ Resolvente SI ∃ P : - Q1 ,... ,Qn tal que ∃Θ = mgu (A , P ) ENTONCES borrar ( Resolvente , A ) anadir ( Resolvente ,Q1 Θ, ... , Qn Θ) aplicar (Θ, Resolvente ) SINO DEVOLVER fail FIN SI FIN MIENTRAS DEVOLVER Θ FIN Donde consideramos que la Resolvente contiene en cada instante el conjunto de objetivos a resolver (en el siguiente ejemplo son resolventes {persona(Juan)}, {madre(P,M1 ), persona(M1 )} y {persona(Rosa)}). P:- Q1 , ..., Qn es una regla (por ejemplo: persona(P):-madre(P,M),persona(M)) y Qi es cada una de las partes de la cola de dicha regla. Θ = mgu(A,P) es el unificador mas general del objetivo de la Resolvente (A) al aplicar la reducción sobre la regla P. Es decir, si hay una regla que simplifique, entonces simplificamos. La función borrar(Resolvente, A) borra el objetivo A de Resolvente, mientras que la función anadir(Resolvente, Q1 Θ, ..., Qn Θ) añade los objetivos indicados a Resolvente. La función aplicar(Θ, Resolvente) aplica sobre el conjunto de objetivos de Resolvente la restricción representada por la sustitución Θ. Ejemplo: Dado el siguiente programa: madre(Juan, Rosa). persona(P ) : −madre(P, M ), persona(M ). persona(Rosa). Temenos el siguiente árbol de resolución: {persona(Juan)} P ← Juan | { madre(P,M1 ), persona(M1 ) } M1 ← Rosa | { persona(Rosa) } {} Existen diferencias entre el algoritmo teórico de resolución y la implementación de la resolución tal y como hacen los intérpretes y como la realizamos en el ejemplo anterior. En el algoritmo teórico no se define ningún orden, mientras que en la práctica se sigue un orden de izquierda a derecha. Además se mantienen el orden en las colas de las reglas al realizar la sustitución. Es decir, en la regla: persona(P ) : −madre(P, M ), persona(M ). la cola madre(), persona() se mantiene siempre en ese orden y no irá nunca persona antes que madre. Esto tampoco está definido en el algoritmo teórico. 4. Mapa de nomenglatura Paradigma imperativo asignación de datos y tipos mov. cabeza lectora en MT Paradigma funcional sustitución β-reducción Paradigma lógico unificación resolución Referencias [1] Teorı́a de autómatas, lenguajes y computación. J. E. Hopcroft y J. D. Ullman, 3a Edición, 2007. 9 [2] Computation as Logic. R. Lalément, 1st Edition, 1993. [3] Logic for computer science. S. Reeves, M. Clarke, 1st Edition, 1990. [4] Programación Lógica. Vilares Ferro, M., Alonso Pardo, M.A. y Valderruten Vidal, A, Ed. Tórculo, 2a edición, 1996. [5] http://www.thefreedictionary.com/inference. Internet, Consulta: enero 2013 [6] https://en.wikipedia.org/wiki/Turing machine. Internet, Consulta: enero 2013 [7] Clase de teorı́a LPR. M. Vilares, 2012. 10