Corrección introducción

This commit is contained in:
Vladimir Lemus 2023-02-21 17:07:05 -06:00
parent 89537dfbe2
commit 43f69d742e
9 changed files with 198 additions and 8 deletions

Binary file not shown.

View File

@ -188,6 +188,113 @@ En esos otros modelos se encuentran las funciones recursivas $\mu$, relacionadas
\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}
\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).

Binary file not shown.

View File

@ -329,11 +329,11 @@ En ese caso se utiliza la evaluación perezosa, su principal característica es
Pongamos un ejemplo, el más sencillo es tratar de obtener todos los naturales, definir una lista de todos ellos es imposible, es infinita, pero podemos trabajar con el generador de esos números:
\lstinputlisting[firstline=1,lastline=5]{prim.py}
\lstinputlisting[firstline=1,lastline=5]{nats.py}
Esto no se ve nada impresionante, estamos sumando un número al anterior, pero esta implementación sencilla ya es nuestra entrada al \emph{infinito y más allá}, porque realmente estamos trabajando con un generador. En gran medida es una función que puede generarnos todos los naturales pero que además podemos operar con ella (nótese que ya estamos hablando de programar con funciones). Y se preguntarán ¿Y cómo la uso? Veamos un ejemplo.
Veamos un algoritmo griego con más de $2000$ años de antigüedad, el colador de Eratostenes. ¿Cómo puedo obtener todos los números primos? Podemos hacerlo por fuerza bruta, es decir, por la simple definición dejar que el programa haga las operaciones, que cheque que cada número solo es múltiplo de el mismo, pero como se imaginarán al poco tiempo la tarea se volverá tardada. A Eratostenes se le ocurrió una manera que lleva menos tiempo: se ponen todos los número naturales sobre la rejilla del colador, en esa primera colada todos los múltiplos de $2$ se quedarán como asientos y los demás número fluirán a la siguiente etapa. En esta etapa todos los múltiplos de $3$ se quedan sedimentados y pasan los siguientes números. El $4$ ya se quedo unh sedimento arriba así que sigue el $5$, y así se continúa.
Veamos un algoritmo griego con más de $2000$ años de antigüedad, el colador de Eratostenes. ¿Cómo puedo obtener todos los números primos? Podemos hacerlo por fuerza bruta, es decir, por la simple definición dejar que el programa haga las operaciones, que cheque que cada número solo es múltiplo de el mismo, pero como se imaginarán al poco tiempo la tarea se volverá tardada. A Eratostenes se le ocurrió una manera que lleva menos tiempo: se ponen todos los número naturales sobre la rejilla del colador, en esa primera colada todos los múltiplos de $2$ se quedarán como asientos y los demás número fluirán a la siguiente etapa. En esta etapa todos los múltiplos de $3$ se quedan sedimentados y pasan los siguientes números. El $4$ ya se quedo un sedimento arriba así que sigue el $5$, y así se continúa.
\begin{table}[h!]
\centering
@ -349,6 +349,62 @@ Veamos un algoritmo griego con más de $2000$ años de antigüedad, el colador d
\label{tab:pascal}
\end{table}
Ahora tratemos de armar el programa utilizando este algoritmo y la evaluación perezosa, partiendo de lo que ya obtuvimos para números naturales y solo seleccionando los primos. Utilizaremos de igual forma la función \emph{yield} de $python$ y obtendremos un generador de números primos.
\lstinputlisting[firstline=1,lastline=12]{nats.py}
Se necesita de la función que genera a los naturales para obtener al generador de los primos pero como se puede ver dentro de la generación de los primos se puede incluir al algortimo de la colareda de Eratóstenes.
Ahora ¿qué tal si queremos aplicarlo al caso del triángulo de Pascal? El deseo es tener un generador de filas que lo haga de forma perezosa, pero que como hemos hecho, podamos manejar ese infinito de filas en potencia por medio de una función.
Debo adelantar que no sé cómo hacerlo, pero lo intenté. Este intento no cuneta como evaluación perezosa pues realmente no hay un generador de filas, sólo se parte de los naturales y se aplica la función del triángulo de Pascal.
\lstinputlisting[firstline=1,lastline=16]{pascalazy.py}
Al parecer hay manera de hacerlo con \emph{dask}, pero como no tengo lo librería necesaria no lo intento.
\subsection*{Programación de más alto orden}
Ahora si quisiéramos operar sobre las filas del triángulo de Pascal ¿qué deberíamos hacer? Quizá la respuesta directa sería asignar lo que obtenemos de nuestra función a una variable, guardándola en memoria y ahora si operar sobre esa variable. Pero recuerden que aquí somos perezosos, ¿porque no operar sobre funciones y ahorrarnos ese paso?
La programación de más alto orden es construir funciones a partir de un funciones, tal como las capas de una cebolla, tener los objetos sobre los que se trabaja definidos a partir de otras funciones. Resumidamente, pasar funciones como argumentos a otras funciones. Esto es útil para ahorrar la definición de procedimientos y hacer funciones que pueden servir para distintas tareas, en ciertos casos puede hacer el código más compacto y fácil de manejar, por supuesto puede haber casos donde complique más las cosas.
Aplicándolo al caso del triángulo de Pascal:
\lstinputlisting[firstline=1,lastline=20]{pascalazy.py}
Definimos dos funciones, una que concatena un $0$ por la izquierda a la fila que digamos, $conc$, y otra que concatena la siguiente fila, $concs$. Nótese que en ambos casos los argumentos son una función ($func$) y el valor de la fila, sólo hasta el moemnto de evaluar definimos la función a dar como argumento, que sería $pascal$.
De esta forma es posible operar sobre las filas sin necesidad de asignarlas a una variable. Al menos para este caso ahorra un poco de líneas al código.
\section*{Concurrencia: programación orientada a objetos y sus problemas}
Sin lugar a dudas cada paradigma de programación ajusta para diversas tareas, ninguno es mejor que el otro. Pereo como aquí nos centraremos en la programación funcional mencionaremos cuales son algunos de los problemas que puede resolver que si no se es muy cuidadoso puede pasar en los lenguajes de programación orientada a objetos.
\subsection*{Concurrencia}
Sabemos que existe la posibilidad de correr programas en paralelo. Para ello dependemos de la arquitectura de la computadora, que tenga más de un procesador y que el lenguaje de programación no lo permita. Realmente lo que hace es que en cada procesador se corre un intérprete del lenguaje de programación por separado, ya otra parte del software se dedicará a juntar los resultados.
Pero hay manera de hacer algo parecido con un sólo procesador, lo que se llama concurrencia. Lo que quiere decir que distintas actividades interdependientes, cada una se ejecuta en su propio paso. Se implemente concurrencia a través de *hilos* (*threads*), un hilo simplemente es un programa que se ejecuta, pero un programa puede tener varios hilos.
Dependiendo del lenguaje de programación es la implementación de la concurrencia. En el caso de $python$ los distintos hilos van tomando turnos para correr ene l mismo procesador. Esto puede provocar un problema a la salid, pues si un hilo requiere de la salida de otro hilo para ejecutarse correctamente, deben conocerse bien los tiempos de cada ejecución, de lo contrario se pueden obtener resultados sin mucho sentido o distintos para un mismo programa en concurrencia.
¿Como puede evitar ese problema desde los mismos lenguajes que implementan la concurrencia?
Si una operación trata de usar una variable aún no acotada (que no se le ha asignado valor o dirección a otro valor) y espera hasta que otra operación o proceso la acote para usarla es un buen comportamiento que se llama flujo de datos.
Algunos de los aspectos relacionados con este problema:
\begin{itemize}
\item \textbf{Flujo de datos:} Se define una célula de memoria (para diferenciar de una variable aunque su estructura es la misma) que guarda memoria para saber como ha cambiado el comportamiento de una función, aprender de su pasado.
\item \textbf{Objetos:} Función con memoria interna. Si la célula solo es accesible internamente se dice que está encapsulada.
\item \textbf{Polimorfismo:} la célula funciona bien ante cualquier implementación mientras la interfase sea la misma.
\item \textbf{Clases:} fábricas de objetos.
\end{itemize}
Con todo esto se conforma la programación orientada a objetos. Las operaciones dentro de clases son llamadas métodos. Al agregar herencia se tiene el esquema completo.
El problema, copmo ya se mencionó surge de la relación del tiempo y el \emph{no determinismo}, que es un error que se da al combinar hilos y estados explícitos. El orden de acceso a los estados puede cambiar de acuerdo a los distintos hilos, dando valores distintos para una ejecución en las mismas condiciones. Sucede cuando no se sabe el tiempo exacto en que se ejecuta una operación. En la máquina de terapia de radiación Therac-25, un problema de concurrencia produjo un error en la cantidad de radiación, provocando la muerte de pacientes.
\begin{thebibliography}{10}
\bibitem{Thompson1996} Thompson, Simon J.. ``Haskell - the craft of functional programming.'' International computer science series (1996).

8
lista-ch.py Normal file
View File

@ -0,0 +1,8 @@
def DoChange(aList):
aList.append(4)
return aList
aList = [1, 2, 3]
print(aList)
print(DoChange(aList))
print(aList)

View File

@ -10,13 +10,7 @@ def pascal(n):
def nats(n):
yield n
yield from nats(n+1)
def primo(s):
n=next(s)
yield n
yield from primo(i for i in s if i%n!=0)
s=nats(1)
print(pascal(next(s)))
p=primo(nats(2))

15
pascaldask.py Normal file
View File

@ -0,0 +1,15 @@
import dask
@dask.delayed
def pascalazy(n):
if n==1:
return [1]
else:
r=pascal(n-1)
x=[0]+r
y=r+[0]
return [i+j for i,j in zip(x,y)]
pascalazy(5).compute()

2
suma.hs Normal file
View File

@ -0,0 +1,2 @@
suma x y = x+y
inc = suma 1

8
suma_unch.py Normal file
View File

@ -0,0 +1,8 @@
def DoChange(x, y):
x = x.__add__(y)
return x
x = 1
print(x)
print(DoChange(x, 2))
print(x)