Search

Search this site:

Programación funcional: Enfrenta a la concurrencia

El tema eje de el presente número de Software Gurú es el manejo de datos a muy gran escala (y disculparán que no use la frase más de moda en la industria, Big Data, habiendo otras igual de descriptivas en nuestro idioma). Al hablar de muy gran escala tenemos que entender que pueden ser juegos de datos mucho mayores de lo que acostumbramos — Estamos hablando de un aumento de por lo tres a seis órdenes de magnitud en la escala de los datos a analizar: Según las definiciones más comunes hoy en día, en el rango entre decenas de terabytes y petabytes.

<p>Dar un salto tan grande nos presenta retos en muy diversas      esferas. Para muchas de las necesidades que enfrentamos, tenemos      que adecuar nuestros procesos de desarrollo, las herramientas      que empleamos, el modelo con el cual almacenamos, solicitamos y      procesamos la información, e incluso el hardware mismo. Enfocaré      este texto a un paradigma de programación que permite enfrentar      a la concurrencia de una forma más natural y menos traumática      de lo que acostumbramos.</p>

<p>Una de las razones por las que este tema ha armado tanto      alboroto en el campo del desarrollo de software es que, si bien      la capacidad de cómputo a nuestro alcance ha crecido de forma      francamente bestial desde casi cualquier métrica en el tiempo      que nos ha tocado vivir como profesionales, nuestra formación      sigue estando basada en un modelo de desarrollo muy difícil de      escalar. Vamos, el problema no lo tienen las computadoras, sino      nosotros los programadores: No sólo tenemos que explicar a la      computadora lo que requerimos que haga, sino que además de ello      tenemos que cuidar que no se tropiece –cual si fuera un      ciempiés– con su propia marcha. Siempre que hablemos de muy gran      escala debemos hablar, sí o sí, de un alto grado de paralelismo      en nuestras aplicaciones. Y por muchos años, el paralelismo fue      precisamente algo de lo que buena parte de los programadores      buscaban escapar, por las complicaciones que conlleva ante una      programación imperativa, necesariamente secuencial.</p>

<p>Las alarmas comenzaron a sonar fuertemente hacia el año 2005,      en que los fabricantes de hardware tuvieron que cambiar su      estrategia (y lograron, sorprendentemente, inyectarle algunos      años más de vida a la ley de Moore, la cual indica que el número      de transistores que componen a un chip se duplica      aproximadamente cada dos años, ritmo que se ha mantenido por 40      años ya): Al migrar el desarrollo de procesadores una estrategia      de multiprocesamiento (CPUs múltiples empaquetados como una sóla      unidad), dejando de lado la carrera de los megahertz, aventaron      la <em>papa caliente</em> hacia el lado de los desarrolladores:      Tendríamos que adecuarnos a una realidad de concurrencia      verdadera y ya no simulada.</p>

<p>Los sistemas multiprocesador no son, claro está, tan      recientes. El gran cambio es que ahora prácticamente todas las      computadoras de rango medio (incluso ya algunos teléfonos      celulares) tienen esta tecnología disponible. Claro está, todo      el software de infraestructura, desde los sistemas operativos,      compiladores y bibliotecas tuvieron que irse adecuando      gradualmente para poder administrar y ofrecer al usuario estos      recursos.</p>

<p>Por muchos años, vivimos sabiéndolo en un mundo de falsa      concurrencia: Una computadora sólo podía hacer una cosa a la      vez, dado que contaba con un sólo procesador. Por la velocidad      de su operación, y por el empeño que se puso en que      los <em>cambios de contexto</em> fueran tan ágiles como fuese      posible, nos daba la impresión de que varias cosas ocurrían al      mismo tiempo. Esto, claro, no debe sorprender a ninguno de      ustedes — El gran reto introducido por el paralelismo real es      manejar correctamente escenarios mucho más complejos de      condiciones de carrera que antes no se presentaban tan      fácilmente. Antes del paralelismo, podíamos indicar al sistema      operativo que nuestro programa estaba por entrar a      una <em>sección crítica</em>, con lo cual éste podía decidir      retirar el control a nuestro programa para entregarlo a otro si      estaba cercano a finalizar su tiempo, o darle una prórroga hasta      que saliera de dicha sección.</p>

<p>Cuando hay más de un procesador (un CPU <em>multicore</em>      o <em>multinúcleo</em> alberga a varios procesadores, en      ocasiones compartiendo elementos como uno de los niveles de      memoria <em>cache</em>), la situación se complica: El sistema      operativo puede, sí, mantener en ejecución a uno de los      programas para reducir la probabilidad de conflictos, pero se      hizo indispensable hacer la colaboración entre procesos algo      explícito.</p>

<p>Claro, esto no fue un desarrollo repentino ni algo      fundamentalmente novedoso. Los <em>mutexes</em> nos han      acompañado por muy largos años, y la programación multihilos      nos ha regalado dolores de cabeza desde hace muchos años. Sin      embargo, en sistemas uniprocesador, la incidencia de condiciones      de carrera era suficientemente baja como para que muchos las      ignoraran.</p>

<p>Una de las razones por las que la concurrencia nos provoca esos      dolores de cabeza es porque nos hemos acostumbrada a      enfrentarnos a ella con las herramientas equivocadas. Un      tornillo puede clavarse a martillazos, y no dudo que haya quien      use destornilladores para meter clavos, pero tendremos mucho      mayor éxito (y un tiempo de entrega mucho más aceptable) si      usamos la herramienta correcta.</p>

<p>Los lenguajes basados en <em>programación funcional</em>      resuelven en buena medida los problemas relacionados con la      concurrencia, y pueden de manera natural desplegarse en un      entorno masivamente paralelo. Sin embargo, requieren un cambio      más profundo en la manera de pensar que, por ejemplo, cuando      adoptamos la adopción de la programación orientada a      objetos.</p>

<p>¿Cuál es la diferencia? Aprendimos a programar de      forma <em>imperativa</em>, con el símil de la lista de      instrucciones para una receta de cocina — Los lenguajes      puramente funcionales son mucho más parecidos a una definición      matemática, en que no hay una secuencia clara de resolución,      sino que una definición de cómo se ve el problema una vez      resuelto, y los datos se encargan de ir marcando el camino de      ejecución. Los lenguajes puramente funcionales tienen una larga      historia (Lisp fue creado en 1958), pero en la industria nunca      han tenido la adopción de los lenguajes imperativos. Hay una      tendencia en los últimos 20 años, sin embargo, de incorporar      muchas de sus características en lenguajes mayormente      imperativos.</p>

<p>La principal característica que diferencía a los lenguajes      funcionales que nos hacen pensar en definiciones matemáticas es      que la llamada a una función <em>no tiene efectos      secundarios</em> — ¿Han depurado alguna vez código multihilos      para darse cuenta que el problema venía de una variable que no      había sido declarada como exclusiva? Con la programación      funcional, este problema simplemente no se presentaría. Esto      lleva a que podamos definir (en AliceML) el cálculo de la serie      de Fibonacci como:</p>

<pre>
  fun fib 0 = 0
    | fib 1 = 1
    | fib n  if (n > 1) = spawn fib(n-1) + fib(n-2);
    | fib _ = raise Domain
</pre>

<p>A diferencia de una definición imperativa, la función es      definida dependiendo de la entrada recibida, y la última línea      nos muestra el comportamiento en caso de no estar contemplado      por ninguna de las condiciones. Y el puro hecho de indicar la      palabra «spawn» indica al intérprete que realice este cálculo en      un hilo independiente (que podría ser enviado a otro procesador,      o incluso a otro nodo, para su cálculo).</p>

<p>Otra de las propiedades de estos lenguajes, las <em>funciones      de órden superior</em> (funciones que toman como argumentos a      otras funciones). Por ejemplo, en Haskell:</p>

<pre>
  squareList = map (^2) list
</pre>

<p>Al darle una lista de números a la función squareList, nos      entrega otra lista, con el cuadrado de cada uno de los elementos      de la lista original. Y, obviamente, esto se puede generalizar a      cualquier transformación que se aplicar iterativamente a cada      uno de los elementos de la lista.</p>

<p>Hay varios tipos de funciones de órden superior, pero en líneas      generales, pueden generalizarse al mapeo (repetir la misma      función sobre los elementos de una lista, entregando otra lista      como resultado) y la reducción (obtener un resultado único por      aplicar la función en cuestión a todos los elementos de la      lista). Y es, de hecho, basándose en juegos de mapeo/reducción      que se ejecutan la mayor parte de las tareas intensivas en      datos en Google.</p>

<p>Podemos encontrar frecuentemente otros dos patrones en estos      lenguajes, aunque por simplicidad no los incluyo en estos      ejemplos: Por un lado, al no tener efectos secundarios, tenemos      la garantía de que toda llamada a una función con los mismos      argumentos tendrá los mismos resultados, por lo que un cálculo      ya realizado no tiene que recalcularse, y podemos guardar los      resultados de las funciones (especialmente en casos altamente      recursivos, como éste). En segundo, la evaluación postergada:      Podemos indicar al intérprete que guarde un apuntador a un      resultado, pero que no lo calcule hasta que éste sea requerido      para una operación inmediata (por ejemplo, para desplegar un      resultado, o para asignarlo a un cálculo no postergable).</p>

<p>Una de las grandes desventajas que enfrentó la programación      funcional es que los lenguajes funcionales puros crecieron      dentro de la burbuja académica, resultando imprácticos para su      aplicación en la industria del desarrollo. Esto ha cambiado      fuertemente. Hoy en día podemos ver lenguajes que gozan de gran      popularidad y han adoptado muchas construcciones derivadas de la      programación funcional, como Python, Ruby o Perl. Hay lenguajes      funcionales que operan sobre las máquinas virtuales de Java      (Clojure) y .NET (F#). Por otro lado, lenguajes como Erlang,      OCaml y Scheme se mantienen más claramente adheridos a los      principios funcionales, pero con bibliotecas estándar y      construcciones más completas para el desarrollo de      aplicaciones.</p>

<p>El manejo de cantidades masivas de datos están llevando a un      pico de interés en la programación funcional. No dejen pasar a      esta interesante manera de ver al mundo - Puede costar algo de      trabajo ajustar nuestra mente para pensar en términos de este      paradigma, pero los resultados seguramente valdrán la pena.</p>

Attachments

As printed in the magazine (page 1) (990 KB)

As printed in the magazine (page 2) (983 KB)