{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'>\n",
    "    &copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados. \n",
    "    Modificado desde 2018-1 al 2026-1 por Equipo Docente IIC2233\n",
    "</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Paradigmas de programación](#paradigmas-de-programación)\n",
    "2. [Programación Funcional](#programación-funcional)\n",
    "    1. [Funciones Puras](#funciones-puras)\n",
    "    2. [Consecuencias de la Programación Funcional](#consecuencias-de-la-programación-funcional)\n",
    "    3. [Conclusión](#conclusión)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Paradigmas de programación\n",
    "\n",
    "A inicios del curso, hablamos sobre los distintos paradigmas de las programación y que cada uno de ellos nos indica un enfoque o estrategias que nos permitirán enfrentarnos a un problema.\n",
    "\n",
    "Recordemos algunos de los paradigmas de programación que existen:\n",
    "\n",
    "- **Procedimental**: la solución se estructura como un programa lineal. Esto es una lista de instrucciones que indican al computador qué se debe hacer con la entrada del programa en cada paso. En _Introducción a la Programación_ programamos de esta manera usando Python.\n",
    "\n",
    "- **Vectorial**: se utiliza principalmente para programas matemáticos donde hay un paralelismo implícito en los cálculos. La programación se realiza secuencialmente y el compilador se encarga de generar paralelismo en las partes donde es posible distribuir el trabajo.\n",
    "\n",
    "- **Declarativa**: el usuario declara un problema a resolver, luego el computador determina la mejor manera de resolver el problema de manera eficiente. Por ejemplo, al consultar una base de datos usando el lenguaje SQL, donde el usuario describe de forma general una pregunta y el computador decide por si mismo cómo mover los datos para responder esa pregunta. Otro ejemplo son los lenguajes que resuelven problemas de optimización, donde se declaran todas las restricciones y función objetivo, y es el computador el encargado de decidir cómo resolver el problema.\n",
    "\n",
    "- **Orientada a Objetos**: esto programas modelan las funcionalidades a través de interacciones entre objetos. Se utilizan los datos/atributos de los objetos y sus comportamientos para dar sentido al programa. Es lo que hemos hecho en el primer capítulo de este curso.\n",
    "\n",
    "- **Funcional**: es programación procedimental de alto nivel. La solución del problema se estructura como un conjunto de funciones. Estas funciones reciben entradas y generan salidas. Las funciones no tienen estado, es decir, el _output_ depende exclusivamente de los datos de entrada y no de otras variables externas que puedan modificar el cómputo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python es un lenguaje ***multiparadigma***, es decir, las soluciones pueden ser escritas de forma procedimental, orientada a objetos o funcional. Así, nuestros programas podrían ser escritos usando los diferentes enfoques de forma simultánea."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Programación Funcional\n",
    "\n",
    "En programación funcional, una función corresponde a lo mismo que en el contexto de las matemáticas, que significa que dado el mismo valor de entrada se genera el mismo y único valor de salida. Dado lo anterior, el valor de retorno de una función depende **solamente** de los parámetros de entrada de la función. \n",
    "\n",
    "En este paradigma todo es visto como el *output* de una función. Además, como el *output* de una función solo depende de su *input*, siempre podemos saber el valor de una variable que guarda el resultado de una función. Bajo ninguna circunstancia esa variable cambiará de valor a menos que le asignemos el *output* a otra función. Estas características otorgan claridad al código que se escribe, pues estamos seguros de que cuando se ejecuta una función no se cambian otros valores fuera de su ámbito de alcance (*scope*).\n",
    "\n",
    "En los siguientes _notebooks_ de esta semana tendremos un acercamiento a lo que es la programación funcional, en el cual aprenderemos a utilizar distintas herramientas que nos serán de utilidad para implementar soluciones que cumplan este paradigma de programación.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Funciones Puras\n",
    "\n",
    "Como indicábamos anteriormente, en el contexto de la programación funcional, las funciones solo dependen de los parámetros de entrada (_input_) al momento de retornar un valor (_output_) y no poseen **efectos secundarios (_side effect_)**; cuando esto sucede, diremos que el código es **puro**. Como **efectos secundarios** entenderemos que es afectar a cualquier cosa que no dependa únicamente de la función.\n",
    "\n",
    "La \"pureza\" mide el grado de dependencia de la función hacia el sistema o el programa, es decir, el contexto en el cual se está ejecutando dicha función:\n",
    "![](img/pureza.png)\n",
    "\n",
    "En resumen, **una función pura retorna lo mismo para el mismo input, y no posee efectos secuntarios**.\n",
    "\n",
    "Veamos algunos ejemplos aplicados:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "mi_lista = [1, 2, 3]\n",
    "\n",
    "# Función impura\n",
    "def agregar_elemento(número: int) -> None:\n",
    "    mi_lista.append(número)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En este caso, dado que la función hace uso de `mi_lista` por lo que depende de algo más que su _input_, esto hace que la función sea **impura**.\n",
    "\n",
    "Una forma de validar lo anterior, es que si ejecutamos la función múltiples veces podremos notar que el resultado de la ejecución cambiará, pese a que no han cambiado los parámetros de entrada de la función:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ejecución 1: [1, 2, 3, 4]\n",
      "Ejecución 2: [1, 2, 3, 4, 4]\n",
      "Ejecución 3: [1, 2, 3, 4, 4, 4]\n",
      "Ejecución 4: [1, 2, 3, 4, 4, 4, 4]\n",
      "Ejecución 5: [1, 2, 3, 4, 4, 4, 4, 4]\n"
     ]
    }
   ],
   "source": [
    "for i in range(1, 6):\n",
    "    agregar_elemento(4)\n",
    "    print(f'Ejecución {i}: {mi_lista}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para evitar que la función `agregar_elemento` sea impura, deberemos asegurar que el acceso a `mi_lista` se haga por medio del _input_ de la función."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ejecución 1: [1, 2, 3] [1, 2, 3, 4]\n",
      "Ejecución 2: [1, 2, 3] [1, 2, 3, 4]\n",
      "Ejecución 3: [1, 2, 3] [1, 2, 3, 4]\n",
      "Ejecución 4: [1, 2, 3] [1, 2, 3, 4]\n",
      "Ejecución 5: [1, 2, 3] [1, 2, 3, 4]\n"
     ]
    }
   ],
   "source": [
    "from copy import deepcopy  # Lo usamos para que \n",
    "# la nueva lista no dependa de la original\n",
    "\n",
    "mi_lista = [1, 2, 3]\n",
    "\n",
    "# Función pura\n",
    "def agregar_elemento(lista: list, número: int) -> list:\n",
    "    copia_lista = deepcopy(lista)\n",
    "    copia_lista.append(número)\n",
    "    \n",
    "    return copia_lista\n",
    "\n",
    "\n",
    "for i in range(1, 6):\n",
    "    nueva_lista = agregar_elemento(mi_lista, 4)\n",
    "    print(f'Ejecución {i}: {mi_lista} {nueva_lista}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora la función `agregar_elemento` solo depende de los parámetros de entrada, entonces ¿podemos decir que la función es pura?\n",
    "> Sí\n",
    "\n",
    "¿Por qué?\n",
    "> Su valor de retorno depende únicamente del valor de entrada\n",
    ">\n",
    "> Además, al solo modificar algo dentro de su *scope*, la ejecución de la función produce no un **efecto secundario (_side effect_)**.\n",
    "\n",
    "En resumen, una función  **una función pura retorna lo mismo para el mismo input, y no posee efectos secundarios**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Dado que ahora tenemos una mayor claridad sobre lo que son las funciones puras, veamos otros casos:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def aumentar(x: int) -> int:\n",
    "    return x + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿La función `aumentar` cumple los criterios de una función pura?\n",
    "\n",
    "* ✅ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? Sí\n",
    "* ✅ ¿Posee efectos secundarios? No\n",
    "\n",
    "Entonces, `aumentar` es una función pura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = ['a', 'b', 'c']\n",
    "\n",
    "def agregar_item(item: str) -> str:\n",
    "    items.append(item)\n",
    "    return item"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿La función `agregar_item` cumple los criterios de una función pura?\n",
    "\n",
    "* ✅ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? Sí, ya que retorna el mismo valor que se recibió\n",
    "* ❌ ¿Posee efectos secundarios? Sí, ya que cambia el contenido de las lista `items`\n",
    "\n",
    "Entonces, `agregar_item` no es una función pura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from random import random\n",
    "\n",
    "def fraccionar_random(x: float) -> float:\n",
    "    return x * random()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿La función `fraccionar_random` cumple los criterios de una función pura?\n",
    "\n",
    "* ❌ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? No, al poseer un valor aleatorio\n",
    "* ❌ ¿Posee efectos secundarios? Sí, al llamar `random`, se altera el siguiente valor a llamar\n",
    "\n",
    "Entonces, `fraccionar_random` no es una función pura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "from random import random, seed\n",
    "\n",
    "def fraccionar_random2(x: float) -> float:\n",
    "    seed(x)\n",
    "    return x * random()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Nota:** `seed()` hace que el siguiente valor obtenido de `random` sea siempre el mismo si es que se usó el mismo parámetro para `seed()`.\n",
    "\n",
    "¿La función `fraccionar_random2` cumple los criterios de una función pura?\n",
    "\n",
    "* ✅ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? Si, al depender de la _seed_\n",
    "* ❌ ¿Posee efectos secundarios? Si, al modificar la _seed_ de _random_, lo que modifica los valores futuros que se hagan a `random()` fuera de la función\n",
    "\n",
    "Entonces, `fraccionar_random2` no es una función pura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from random import Random\n",
    "\n",
    "def fraccionar_random3(x: float) -> float:\n",
    "    NuevoRandom = Random(x)\n",
    "    return x * NuevoRandom.random()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Nota:** `Random` recibe como parámetro una *seed*, que hace que los valores obtenidos después de cada llamado sean siempre los mismos si es que se usó la misma *seed*.\n",
    "\n",
    "¿La función `fraccionar_random3` cumple los criterios de una función pura?\n",
    "\n",
    "* ✅ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? Si, al depender de la _seed_\n",
    "* ✅ ¿Posee efectos secundarios? No, al crear un nuevo random localmente\n",
    "\n",
    "Entonces, `fraccionar_random3` es una función pura."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def aumentar_con_print(x: int) -> int:\n",
    "    print('Voy a retornar', x, 'aumentado en uno')\n",
    "    return x + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿La función `aumentar_con_print` cumple los criterios de una función pura?\n",
    "\n",
    "* ✅ Si se mantiene el _input_, ¿siempre entrega el mismo _output_? Si\n",
    "* ❌ ¿Posee efectos secundarios? Si, al hacer print afecta lo que muestra el programa\n",
    "\n",
    "Entonces, `aumentar_con_print` no es una función pura.\n",
    "\n",
    "Como puedes haber visto, los efectos secundarios tienen un alcance mayor al esperado, por ejemplo, una función pura no puede crear archivos ni modificarlos, ya que eso puede alterar otros elementos del programa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Consecuencias de la Programación Funcional\n",
    "\n",
    "En base a todo lo que hemos visto, ¿cuáles serian las consecuencias de implementar funciones puras?\n",
    "\n",
    "1. Ya no se modifican elementos externos a una función.  \n",
    "   ➡️ Ahora, podemos descartar revisar funciones puras cuando tenemos que un valor o una estructura cambia inesperadamente.\n",
    "\n",
    "2. Ahora el output solo depende del input.  \n",
    "   ➡️ Esto permite que si el programa recuerda el input de una función pura, no necesita llamar nuevamente a la función.\n",
    "\n",
    "3. Dada la ausencia de efectos secundarios, iteraciones como `for` o `while` se vuelven mas amigables de modelar.  \n",
    "   ➡️ Nuestro código se vuelve mas amigable para modelar. \n",
    "\n",
    "Veamos un ejemplo del segundo punto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "buscando el número 4\n",
      "buscando el número 3\n",
      "buscando el número 2\n",
      "buscando el número 1\n",
      "buscando el número 0\n",
      "buscando el número 1\n",
      "buscando el número 2\n",
      "buscando el número 1\n",
      "buscando el número 0\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def fibonacchi(n: int) -> int:\n",
    "    print('buscando el número', n)\n",
    "    if n < 2:\n",
    "        return n\n",
    "    return fibonacchi(n-1) + fibonacchi(n-2)\n",
    "\n",
    "fibonacchi(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como puedes ver, hay varias funciones repetidas, y sin contar los prints que están para mostrar cómo corre el código, solo se desea el output a partir del input. Podemos aprovechar de decirle a Python que no corra nuevamente la función con el decorador `@cache`, y que asuma que el output se mantiene dado el mismo input:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "buscando el número 4\n",
      "buscando el número 3\n",
      "buscando el número 2\n",
      "buscando el número 1\n",
      "buscando el número 0\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from functools import cache\n",
    "\n",
    "@cache\n",
    "def fibonacchi_cached(n: int) -> int:\n",
    "    print('buscando el número', n)\n",
    "    if n < 2:\n",
    "        return n\n",
    "    return fibonacchi_cached(n-1) + fibonacchi_cached(n-2)\n",
    "\n",
    "fibonacchi_cached(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Como puedes ver no se imprime tantas veces como antes, y esto es porque el decorador `@cache` funciona correctamente con las **funciones puras**, y el hacer print hace que la función no sea pura. \n",
    "\n",
    "Ahora, hacemos que la función sea pura y aumentamos el número con el que llamamos a la función para aprovechar el decorador:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "83428786095010233039452893451168885358856822423517291331018551725755973092961397681432523209335078083037082049842613293369652888469867204072869026512035048078160170454915915213979475203909274364258193729858"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@cache\n",
    "def fibonacchi_cached(n: int) -> int:\n",
    "    if n < 2:\n",
    "        return n\n",
    "    return fibonacchi_cached(n-1) + fibonacchi_cached(n-2)\n",
    "\n",
    "fibonacchi_cached(987)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Además, el que nuestras funciones sean puras también tiene consecuencias a la hora de plantear un código:\n",
    "1. Ya no pensaremos en almacenamiento de variables, sino que en su modificación.\n",
    "2. El razonamiento para programar se basará en pensar **flujos de transformación de datos (_data flow_)**.\n",
    "3. Cada función recibe un estado particular en el que se encuentra un dato. A partir de esto, la función lo transforma y lo retorna.\n",
    "\n",
    "![](img/data_flow.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusión\n",
    "\n",
    "La correcta implementación de una solución funcional, tiene grandes efectos a la hora de plantear una solución que cumpla con todo lo visto anteriormente.\n",
    "\n",
    "Para efectos de facilitar el aprendizaje de este curso, el material entregado en los próximos _notebooks_ puede presentar *prints* dentro de funciones que debiesen ser puras."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
