notas-tsfc/calculo_lambda.tex

432 lines
20 KiB
TeX

\documentclass[10pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[spanish]{babel}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{hyperref}
\usepackage{graphicx}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{caption}
\usepackage{pgf}
\usepackage{tikz}
\usetikzlibrary{automata,positioning,arrows}
\definecolor{codegreen}{rgb}{0,0.6,0}
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
\definecolor{codepurple}{rgb}{0.58,0,0.82}
\definecolor{backcolour}{rgb}{0.95,0.95,0.92}
\definecolor{darkblue}{rgb}{0,0,.75}
\lstdefinestyle{mystyle}{
backgroundcolor=\color{backcolour},
commentstyle=\color{codegreen},
keywordstyle=\color{magenta},
numberstyle=\tiny\color{codegray},
stringstyle=\color{codepurple},
basicstyle=\ttfamily\footnotesize,
breakatwhitespace=false,
breaklines=true,
captionpos=b,
keepspaces=true,
numbers=left,
numbersep=5pt,
showspaces=false,
showstringspaces=false,
showtabs=false,
tabsize=2
}
\lstset{style=mystyle}
\lstloadlanguages{Matlab} %use listings with Matlab for Pseudocode
\lstnewenvironment{PseudoCode}[1][]
{\lstset{language=Matlab,basicstyle=\scriptsize, keywordstyle=\color{darkblue},numbers=left,xleftmargin=.04\textwidth,#1}}
{}
\renewcommand{\rmdefault}{ptm}
%\usepackage[all,cmtip]{xy}
%\usepackage{graphicx}
\author{Programación funcional para la física computacional}
\title{Cálculo $\lambda$ y programación funcional}
\begin{document}
\maketitle
\section*{Máquinas de Turing}
La máquina de Turing es uno de los modelos de computación más usados, es lo más cercano a una computadora (como la que usaron para bajar este pdf, y quizá hasta para leerlo), pero no es el único modelo. Ahora lo mencionó sólo para dar entrada a la idea del cálculo $\lambda$, para entender las cosas. No sé si será el método más pedagógico, o el más sencillo, pero es el estándar en la mayoría de libros del tema (además de que Turing es el más famoso de los matemáticos que trabajaron inicialmente en el área). Los modelos son equivalentes, así que no perderemos generalidad.
Una máquina de Turing se compone de un conjunto finito de estados $Q$, una cinta semi-infinita limitada por la izquierda con el símbolo $\vdash$ e ilimitada por la derecha (este límite izquierdo es para saber donde empieza la cinta) y una cabeza que puede moverse a izquierda y derecha, capaz de leer y escribir caracteres en la cinta\footnote{Son muy jóvenes para recordar los casetes quizá, aunque por ahí hay unos intentos retros de revivirlos, pero si alguna vez han visto uno con su respectivo reproductor podrán notar que cuenta con una cinta magnética de color café o negro, que es leída por una cabeza también magnética. Era un formato de no muy buena calidad, de riesgo pues un imán de potencia suficiente podía dañar la cinta y los carretes podían provocar accidentes como enrollarse o atorarse. Hubo computadoras que usaban casetes para leer programas, para una crónica al respecto echen un ojo a: \url{https://www.xataka.com/historia-tecnologica/cuando-los-videojuegos-venian-en-cassette-y-habia-que-rebobinarlos-para-poder-jugar}}. Las palabras de entrada, de longitud finita, se escriben sobre la cinta de izquierda a derecha (como escribimos nosotros). Al terminar la palabra de entrada en el resto de casillas de la cinta para distinguir que no contienen caracter alguno está pre-escrito el símbolo $\textvisiblespace$. Un esquema ejemplificando esta disposición se muestra en la figura \ref{fig:tur}.
\begin{figure}[h]
\begin{center}
\begin{tikzpicture}[every node/.style={block},
block/.style={minimum height=1.5em,outer sep=0pt,draw,rectangle,node distance=0pt}]
\node (A) {$a$};
\node (B) [left=of A] {$\ldots$};
\node (C) [left=of B] {$\vdash$};
\node (D) [right=of A] {$\ldots$};
\node (E) [right=of D] {$\textvisiblespace$};
\node (F) [above = 0.75cm of A,draw=red,thick] {\textsf Q};
\draw[-latex] (F) -- (A);
%\draw[-latex,blue] ($(F.east)!0.5!(A.east)$) -- ++(7mm,0);
\draw (C.north west) -- ++(-1cm,0) (C.south west) -- ++ (-1cm,0)
(E.north east) -- ++(1cm,0) (E.south east) -- ++ (1cm,0);
\end{tikzpicture}
\caption{Esquema mecánico de una máquina de Turing.}
\end{center}
\label{fig:tur}
\end{figure}
Podemos dar una definición formal:
\newtheorem{defi}{Definición}
\begin{defi}
Una máquina de Turing determinista, de cinta única es una 9-tupla (yo lo traduciría como un noneto o enéada, pero quizá no es la terminología) descrita como:
\begin{equation*}
M=(Q,\Sigma, \Gamma,\vdash,\textvisiblespace,\delta,s,t,r)
\end{equation*}
con:
\begin{itemize}
\item $Q$ es el conjunto finito de estados
\item $\Sigma$ es el alfabeto de entrada (finito)
\item $\Gamma$ es el alfabeto de cinta (finito), con $\Sigma \subseteq \Gamma$
\item $\vdash \in \Gamma - \Sigma$ el símbolo de inicio de la cinta
\item $\textvisiblespace \in \Gamma - \Sigma$ el símbolo de espacio en blanco
\item $\delta: Q\times \Gamma \rightarrow Q\times \Gamma \times \{ \rightarrow,\leftarrow \}$ la función de transición
\item $s\in Q$ el estado inicial
\item $t\in Q$ el estado de aceptación
\item $r\in Q$ el estado de rechazo.
\end{itemize}
\end{defi}
\subsection*{Un ejemplo}
Sea la máquina de Turing que decida el lenguaje $A=\{ 0^{2^n}|n\geq 0 \}$, es decir, el lenguaje con puras cadenas de $0$'s que su longitud sea una potencia de $2$.
\begin{center}
\begin{tabular}{ | c | c | c | c | c | c }
\hline
& $\vdash$ & $0$ & $x$ & $\textvisiblespace$ \\ \hline
$s$ & $(s,\vdash,\rightarrow)$ & $(q_1,\textvisiblespace,\rightarrow)$ & $(r,-,-)$ & $(r,-,-)$ \\ \hline
$q_1$ & & $(q_2,x,\rightarrow)$ & $(q_1,x,\rightarrow)$ & $(t,-,-)$ \\ \hline
$q_2$ & & $(q_3,0,\rightarrow)$ & $(q_2,x,\rightarrow)$ & $(q_4,\textvisiblespace,\leftarrow)$ \\ \hline
$q_3$ & & $(q_2,x,\rightarrow)$ & $(q_3,x,\rightarrow)$ & $(r,-,-)$ \\ \hline
$q_4$ & & $(q_4,0,\leftarrow)$ & $(q_4,x,\leftarrow)$ & $(q_1,\textvisiblespace,\rightarrow)$ \\
\hline
\end{tabular}
\end{center}
\begin{figure}[h]
\begin{center}
\begin{tikzpicture}%[shorten >=1pt,node distance=2cm,on grid,auto]
\node[state,initial,initial text= ] (S) {$S$};
\node[state,xshift=1cm,right=of S] (q_1) {$q_1$};
\node[state,above right=of q_1] (q_4) {$q_4$};
\node[state,below right=of q_4] (q_2) {$q_2$};
\node[state,below=of S] (r) {$r$};
\node[state,below=of q_1] (t) {$t$};
\node[state,below=of q_2] (q_3) {$q_3$};
\path [-stealth,thick]
(S) edge node[yshift=-0.3cm]{$0\Rightarrow \textvisiblespace, \rightarrow$} (q_1)
edge node[xshift=-0.5cm] {$\begin{array}{c l}
\textvisiblespace \Rightarrow - \\
x\Rightarrow -
\end{array}$} (r)
(q_1) edge node[yshift=-0.3cm]{$0\Rightarrow x,\rightarrow$} (q_2)
edge [loop above] node {$x\Rightarrow \rightarrow$} ()
edge node[xshift=-0.5cm]{$\textvisiblespace \Rightarrow -$} (t)
(q_2) edge[loop above] node{$x\Rightarrow \rightarrow$} ()
edge [bend right] node[xshift=-0.5cm]{$0\Rightarrow \rightarrow$} (q_3)
edge node{$\textvisiblespace \Rightarrow \leftarrow$} (q_4)
(q_3) edge[bend right] node[xshift=0.5cm]{$0\Rightarrow x,\rightarrow$} (q_2)
edge [loop right] node{$x\Rightarrow \rightarrow$} ()
edge [bend left] node[yshift=0.3cm]{$\textvisiblespace \Rightarrow -$} (r)
(q_4) edge[loop above] node{$\begin{array}{c l}
0 \Rightarrow \leftarrow \\
x\Rightarrow \leftarrow
\end{array}$} ()
edge node{$\textvisiblespace \Rightarrow \rightarrow$} (q_1);
\end{tikzpicture}
\caption{Diagrama de estados de la máquina de Turing descrita.}
\label{fig:tur3}
\end{center}
\end{figure}
\section*{Otros modelos computacionales}
El modelo de máquina de Turing ajusta muy bien para cuando se trabaja de manera teórica con computadoras, pero quizá pueda parecer demasiado mecánico y no se parece a como operamos con computadoras hoy en día. Existen más modelos que sonm equivalentes, sabemos de esa equivalencia por la \textbf{tesis Church-Turing}.
\newtheorem{church}{Tesis de Church-Turing}
\begin{church}
Toda función es efectivamente calculable sí y sólo sí es calculable por una máquina de Turing.
\end{church}
En esos otros modelos se encuentran las funciones recursivas $\mu$, relacionadas dierctamente al trabajo de Gödel al preguntarse ¿cuál es el mínimo de funciones necesarias para definir a todas las funciones computables?
\begin{enumerate}
\item \emph{Sucesor}. La función $\mathbf{s}:\mathbb{N} \rightarrow \mathbb{N}$ dadas por $\mathbf{s}(x)=x+1$ es computable.
\item \emph{Cero}. La función $\mathbf{z}: \mathbb{N}^0 \rightarrow \mathbb{N}$ dada por $\mathbb{z}()=0$ es computable.
\item \emph{Proyecciones}. Las funciones $\pi_k^n: \mathbb{N}^n \rightarrow \mathbb{N}$ dadas por $\pi_k^n(x_1,...,x_n) = x_k,\ 1\leq k \leq n$, son computables.
\item \emph{Composición}. Si $f:\mathbb{N}^k \rightarrow \mathbb{N}$ y $g_1,...g_k:\mathbb{N}^n \rightarrow \mathbb{N}$ son computables, entonces también lo es la función $f\circ (g_1,...,g_k):\mathbb{N}^n \rightarrow\mathbb{N}$ que en la entrada $\overline{x}= x_1,...,x_n$, da
\begin{equation*}
f(g_1(\overline{x}),...,g_k(\overline{x}))
\end{equation*}
\item \emph{Recursión primitiva}. Si $h_i:\mathbb{N}^{n} \rightarrow \mathbb{N}$ y $g_i:\mathbb{N}^{n+k+1} \rightarrow \mathbb{N}$ son computables, $1\leq i \leq k$, entonces también lo son las funciones $f_i:\mathbb{N}^{n+1} \rightarrow \mathbb{N},\ 1\leq i \leq k$, definidas por inducción mutua de la siguiente manera:
\begin{align*}
f_i(0,\overline{x}) &\overset{def}{=} h_i(x) \\
f_i(x+1,\overline{x}) &\overset{def}{=} g_i(x,\overline{x},f_1(x,\overline{x}),...,f_k(x,\overline{x})),
\end{align*}
donde $\overline{x}=x_1,...,x_n$.
\item \emph{Minimización no acotada}. Si $g:\mathbb{N}^{n+1} \rightarrow \mathbb{N}$ es computable, entonces también lo es la función $f:\mathbb{N}^n \rightarrow $ que con la entrada $\overline{x}=x_1,...,x_n$ de al menos $y$ tal que $g(z,\overline{x})$ esté definida para todas las $z\leq y$ y $g(y,\overline{x})=0$ si tal $y$, y está indefinida de otra manera. Esto se denota como:
\begin{equation*}
f(\overline{x})=\mu y.(g(y,\overline{x})=0)
\end{equation*}
\end{enumerate}
\section*{Cálculo $\lambda$}
Ya en la introducción habíamos mencionado una forma de ver a las funciones matemáticas como cajas negras, figura \ref{fig:func}. Ahora vamos un paso adelante a definir de manera abstracta lo que es una función, una \emph{abstracción funcional} y como aplicamos las funciones desde una perspectiva abstracta y meramente matemática.
\begin{center}
\begin{figure}[h!]
\begin{tikzpicture}
\draw[black, very thick] (0,0) rectangle (3,2);
\draw[-latex,line width=2pt,black] (-0.8,0.5)--(0,0.5);
\draw[-latex,line width=2pt,black] (-0.8,1.5)--(0,1.5);
\draw[-latex,line width=2pt,black] (3,1)--(3.8,1);
\draw[black, very thick] (6,0) rectangle (9,2);
\filldraw[black] (7.5,1.5) circle (0pt) node[below]{Producto escalar};
\draw[-latex,line width=2pt,black] (5.2,0.5)--(6,0.5);
\filldraw[black] (5.2,0.5) circle (1pt) node[below]{$(3,2,1)$};
\draw[-latex,line width=2pt,black] (5.2,1.5)--(6,1.5);
\filldraw[black] (5.2,1.5) circle (1pt) node[above]{$(2,1,2)$};
\draw[-latex,line width=2pt,black] (9,1)--(9.8,1);
\filldraw[black] (9.8,1) circle (0pt) node[above]{$10$};
\end{tikzpicture}
\caption{Una función representada como caja negra.}
\end{figure}
\label{fig:func}
\end{center}
Las características del cálculo $\lambda$ pueden listarse:
\begin{itemize}
\item Solo depende de funciones. Si no se puede escribir como función, no se puede incluir.
\item No tiene estado o efectos laterales
\item El orden de evaluación no es relevante
\item Todas las funciones son unitarias, sólo toman un argumento
\end{itemize}
El cálculo $\lambda$ fue iniciado en la década de los 30s del siglo $XX$ por Alonso Church. No fue la única persona trabajando en el área, Haskell Curry trabajo en la lógica combinatoria, Gödel en las funciones recursivas $\mu$ en las que podemos encontrar algunas similitudes, Emil Post en el sistema canónico de Post, o sistema de reescritura y Alan Turing con las máquinas de Turing. Todos ellos de alguna manera trataron de definir qué era un algoritmo y darle una estructura formal\footnote{Como vimos en la sección pasada los algoritmos existen incluso desde hace 2000 años, pero no había una teoría formal de ellos.}. Las funciones recursivas $\mu$ y las máquinas de Turing ya las vimos de rápido, en lo que sigue nos centraremos en la parte del trabajo de Church y Curry. Pero para ser justos hay que mencionar como funciona el sistema canónco de Post.
Se parte de una cantidad finitamente grande de cadenas de caracteres que serán manipuladas, de manera repetida son transformadas al aplicar un conjunto finito de reglas, formando un lenguaje regular. Al ser Turing completo, es equivalente a trabajar con máquinas de Turing. Ya sólo se estudia con interés histórico pues hay formalismos que incluyen las ideas de Post más usados hoy en día.
La idea de Post al crear el cálculo $\lambda$ era demostrar que el problema de la decisión de Hilbert no era resolvible por la aritmética de Peano. Lo que obtuvo fue un sistema para estudiar las matemáticas de una forma más general.
Con las reglas antes mencionadas sólo resta decir que para construir todas las funciones matemáticas en el cálculo $\lambda$ basta usar tres operaciones:
\begin{itemize}
\item Crear funciones que recibirán variables
\item Asociar una variable a la expresión
\item Aplicar una función a un argumento
\end{itemize}
¿Porque la diferencia entre asociar una variable y aplicar a un argumento? Tal como cuando definimos una función matemática del estilo $f(x)=x^2-3x$ las variable $x$ es más un marco donde eventualmente puede entrar un valor, alevaluar la ecuación en un número es que estamos aplicando la función a un argumento. Las variables son términos $\lambda$ y son la base de las definiciones deductivas del cálculo $\lambda$.
En el trato normal de las variables en el cálculo $\lambda$ los tipos no entran en juego, está des-tipado (no sé si así se dice, pero suena cagado).
Imaginemos que se tienen dos términos $\lambda$ $M$ y $N$, $MN$ también es un término $\lambda$, podría ser por ejemplo que $M$ es una función y $N$ una entrada (podría ser también una función). La forma más formal de escribirlo es $(M)N$ donde se dice que $N$ es una entrada aplicada a la función $M$. El paréntesis es operador \emph{aplicación} y se asocia a la izquierda (MN)P.
Pero realmente el orden de la aplicación es de izquierda a derecha, por ejemplo, si tenemos $\lambda x.E(x)$ lo que quiere decir es que con la entrada $x$ la función calcula $E(x).$
Veamos un ejemplo concreto. Se tiene:
\begin{lstlisting}{language=Haskell}
inc(x) = x+1
\end{lstlisting}
Lo que quiere decir que para incrementar $x$ se le suma $1$
\begin{lstlisting}{language=Haskell}
(x) -> x+1
\end{lstlisting}
Pero usando ya directamente la abstracción
\begin{equation*}
\lambda x.(x+1)
\end{equation*}
Si ahora tenemos la función
\begin{lstlisting}{language=Haskell}
sumcuad(x,y) = x^2+y^2
\end{lstlisting}
Que se traduce a
\begin{lstlisting}{language=Haskell}
(x,y)-> x^2+y^2
\end{lstlisting}
¿Cómo se pondría en la abastracción?
\begin{equation*}
\lambda x.(\lambda y.(x^2+y^2))
\end{equation*}
¡Muy bien! ¿Y ahora como se evalúa (aplicar a un argumento)? Para el caso del incremento al número $8$:
\begin{equation*}
\lambda x.(x+1)8
\end{equation*}
Vamos a un caso más abstracto, sea la composición de las funciones $f$ y $g$
\begin{equation*}
\lambda x.f(gx)
\end{equation*}
Pero en ese caso solo estamos dando como entrada a $x$ ¿y si quiero dejarlo como el esqueleto de una composición done también puedo darle como entrada las funciones $f$ y $g$?
\begin{equation*}
\lambda f.\lambda g. \lambda x. f(gx)
\end{equation*}
Con ese esqueleto ahora hagamos la composición de la función sucesor:
\begin{align*}
&(\lambda f.\lambda g. \lambda x. f(gx))(\lambda y.(y+1))(\lambda z.(z+1)) \\
&\rightarrow (\lambda g. \lambda x. ((\lambda y.(y+1))(gx)))(\lambda z.(z+1)) \\
&\rightarrow \lambda x.((\lambda y.(y+1))((\lambda z.(z+1))x))) \\
&\rightarrow \lambda x.((\lambda y.(y+1))(x+1)) \\
&\rightarrow \lambda x.((x+1)+1)
\end{align*}
\subsection*{Calculo $\lambda$ \emph{tipado}}
Una variante del cálculo $\lambda$ admite que se especifique el tipo de las variables a dar como argumento, así las funciones definidas se restringen al dominio dado.
Si en la función incremento sólo queremos operar sobre naturales
\begin{equation*}
\lambda x:\nu.(x+1)
\end{equation*}
Eso es el estilo Church, se infiere que al ser la entrada naturales la salida son naturales, pero el estilo Curry hace que toda la función sea definida dentro de los naturales
\begin{equation*}
(\lambda x.(x+1))\nu \rightarrow \nu
\end{equation*}
Si hay más de una variable la versión Post sería
\begin{equation*}
\lambda x:\nu.(\lambda y:\nu.(x^2+y^2))
\end{equation*}
Y la versión Curry
\begin{equation*}
\lambda x.(\lambda y.(x^2+y^2)):\nu \rightarrow \nu \rightarrow \nu
\end{equation*}
\subsection*{Operaciones de reducción}
Vamos ahora a expresar una función $\lambda$ en su forma más simple y pura
\begin{itemize}
\item \textbf{Reducción $\alpha$}: Renombrar variables. Se dice que dos funciones son $\alpha equivalentes$ cuando lo único que cambia es el nombre d ela variable ($\lambda x.(x+1)$ y $\lambda a.(a+1)$).
\item \textbf{Reducción $\beta$}: Remplaza variables por argumentos. Como ya hicimos líneas arriba.
\item \textbf{Reducción $\eta$}:
\end{itemize}
\section*{Programación funcional en \emph{Python}}
Declaraciones en lugar de procedimientos paso a paso.
Le decimos que hacer pero no como hacerlo.
Haskell: datos inmutables
Vemos en python:
\begin{lstlisting}{language=Python}
x=1
oldID=id(x)
x=x+1
id(x)==oldID
\end{lstlisting}
La falta de estado, como sucede en Haskell, conlleva la desventaja de la falta de memoria.
\section*{Funciones en Haskell}
Dos opciones, \emph{curriado} y \emph{no curriado}.
La definición de una *non-curried*
\begin{lstlisting}{language=Haskell}
suma (x,y) = x+y
\end{lstlisting}
A partir de la función suma definamos la función sucesor:
\begin{lstlisting}{language=Haskell}
suc (x) = suma (x,1)
\end{lstlisting}
Si esto mismo trataramos de hacer en $python$, para definir una operación que deja sin cambio una variable, se tiene que hacer:
\lstinputlisting[firstline=1,lastline=9]{suma_unch.py}
Es una definición especial de la suma que lo permite, pero no es la norma, por ejemplo:
\lstinputlisting[firstline=1,lastline=9]{lista-ch.py}
Regresando a Haskell, definamos las operaciones de suma e incremento de forma \emph{curriada}, es decir, definimos una función que sólo necesita un argumento y regresa otra función. Cierrra \textbf{GHCi} y vuelve a abrir. Ahora define:
\begin{lstlisting}{language=Haskell}
suma x y = x+y
\end{lstlisting}
Aquí parece que solo quitamos los paréntesis, pero no. Para notar la diferenbcia ahora definimos la función incremento como:
\begin{lstlisting}{language=Haskell}
inc = suma 1
\end{lstlisting}
Se puede usar *curry* y *uncurry* para pasar de una a la otra. Aprovecha para jugar con map.
Como haríamos esto mismo en $python$:
\begin{lstlisting}{language=Python}
def suma(x,y):
return x+y
\end{lstlisting}
Para definir el incremento en uno:
\begin{lstlisting}{language=Python}
def inc(x):
return suma (x,1)
\end{lstlisting}
\begin{thebibliography}{10}
\bibitem{Thompson1996} Thompson, Simon J.. ``Haskell - the craft of functional programming.'' International computer science series (1996).
\bibitem{VanRoy2009} Van Roy, Peter. ``Programming paradigms for dummies: what every programmer should know.'' (2009).
\bibitem{VanRoy2004} Van Roy, Peter, Haridi, Seif. ``Concepts, Techniques, and Models of Computer Programming''. *The MIT Press*, (2004). ISBN: 0262220695
\bibitem{Kowalski1988} Kowalski, Robert. ``The early years of logic programming'',\textit{Communications of the ACM}, \textbf{31}, 1, (1988).
\bibitem{python_concurrency} \url{https://realpython.com/python-concurrency/}, revisado el 15 de febrero de 2023.
\end{thebibliography}
\end{document}