Entender realmente la diferencia entre procedimental y funcional

114

Realmente estoy teniendo dificultades para entender la diferencia entre los paradigmas de programación funcional y de procedimiento .

Aquí están los dos primeros párrafos de la entrada de Wikipedia sobre programación funcional :

En informática, la programación funcional es un paradigma de programación que trata la computación como la evaluación de funciones matemáticas y evita estados y datos mutables. Enfatiza la aplicación de funciones, en contraste con el estilo de programación imperativa, que enfatiza los cambios de estado. La programación funcional tiene sus raíces en el cálculo lambda, un sistema formal desarrollado en la década de 1930 para investigar la definición de funciones, la aplicación de funciones y la recursividad. Muchos lenguajes de programación funcionales pueden verse como elaboraciones del cálculo lambda.

En la práctica, la diferencia entre una función matemática y la noción de "función" utilizada en la programación imperativa es que las funciones imperativas pueden tener efectos secundarios, cambiando el valor del estado del programa. Debido a esto, carecen de transparencia referencial, es decir, una misma expresión de lenguaje puede dar como resultado valores diferentes en momentos diferentes según el estado del programa en ejecución. Por el contrario, en el código funcional, el valor de salida de una función depende solo de los argumentos que se ingresan a la función, por lo que llamar a una función fdos veces con el mismo valor para un argumento xproducirá el mismo resultadof(x)ambas veces. La eliminación de efectos secundarios puede facilitar la comprensión y la predicción del comportamiento de un programa, que es una de las motivaciones clave para el desarrollo de la programación funcional.

En el párrafo 2 donde dice

Por el contrario, en el código funcional, el valor de salida de una función depende solo de los argumentos que se ingresan a la función, por lo que llamar a una función fdos veces con el mismo valor para un argumento xproducirá el mismo resultado en f(x)ambas ocasiones.

¿No es ese el mismo caso exacto para la programación procedimental?

¿Qué se debe buscar en procedimientos frente a funcionales que se destacan?

Filoxofero
fuente
1
El enlace "Charming Python: Programación funcional en Python" de Abafei estaba roto. Aquí hay un buen conjunto de enlaces: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat
Otro aspecto de esto es nombrar. P.ej. en JavaScript y Common Lisp usamos el término función a pesar de que se permiten efectos secundarios y en Scheme lo mismo se llama constantemente proceduere. Una función CL que es pura se puede escribir como un procedimiento de esquema funcional puro. Casi todos los libros sobre Scheme usan el término procedimiento ya que es el tyerm usado en el estándar y no tiene nada que ver con que sea procedimental o funcional.
Sylwester

Respuestas:

276

Programación funcional

La programación funcional se refiere a la capacidad de tratar las funciones como valores.

Consideremos una analogía con los valores "regulares". Podemos tomar dos valores enteros y combinarlos usando el +operador para obtener un nuevo entero. O podemos multiplicar un número entero por un número de punto flotante para obtener un número de punto flotante.

En la programación funcional, podemos combinar dos valores de función para producir un nuevo valor de función utilizando operadores como componer o lift . O podemos combinar un valor de función y un valor de datos para producir un nuevo valor de datos utilizando operadores como map o fold .

Tenga en cuenta que muchos lenguajes tienen capacidades de programación funcional, incluso lenguajes que normalmente no se consideran lenguajes funcionales. Incluso el abuelo FORTRAN admitía valores de función, aunque no ofrecía muchos operadores de combinación de funciones. Para que un lenguaje se llame "funcional", necesita abarcar las capacidades de programación funcional a lo grande.

Programación procedimental

La programación por procedimientos se refiere a la capacidad de encapsular una secuencia común de instrucciones en un procedimiento de modo que esas instrucciones puedan invocarse desde muchos lugares sin tener que recurrir a copiar y pegar. Como los procedimientos fueron un desarrollo muy temprano en la programación, la capacidad está casi invariablemente vinculada con el estilo de programación exigido por la programación en lenguaje ensamblador o máquina: un estilo que enfatiza la noción de ubicaciones de almacenamiento e instrucciones que mueven datos entre esas ubicaciones.

Contraste

Los dos estilos no son realmente opuestos, solo son diferentes entre sí. Hay lenguajes que abarcan completamente ambos estilos (LISP, por ejemplo). El siguiente escenario puede dar una idea de algunas diferencias en los dos estilos. Escribamos un código para un requisito sin sentido en el que queremos determinar si todas las palabras de una lista tienen un número impar de caracteres. Primero, estilo procedimental:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Daré por sentado que este ejemplo es comprensible. Ahora, estilo funcional:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Trabajando desde adentro hacia afuera, esta definición hace lo siguiente:

  1. compose(odd, length)combina las funciones oddy lengthpara producir una nueva función que determina si la longitud de una cadena es impar.
  2. map(..., words)llama a esa nueva función para cada elemento en words, finalmente devuelve una nueva lista de valores booleanos, cada uno indicando si la palabra correspondiente tiene un número impar de caracteres.
  3. apply(and, ...)aplica el operador "y" a la lista resultante, y combina todos los valores booleanos para obtener el resultado final.

Puede ver en estos ejemplos que la programación de procedimientos se preocupa mucho por mover valores en variables y describir explícitamente las operaciones necesarias para producir el resultado final. En contraste, el estilo funcional enfatiza la combinación de funciones necesarias para transformar la entrada inicial en la salida final.

El ejemplo también muestra los tamaños relativos típicos de código de procedimiento versus funcional. Además, demuestra que las características de rendimiento del código de procedimiento pueden ser más fáciles de ver que las del código funcional. Considere: ¿las funciones calculan las longitudes de todas las palabras de la lista o cada una se detiene inmediatamente después de encontrar la primera palabra de longitud uniforme? Por otro lado, el código funcional permite una implementación de alta calidad para realizar una optimización bastante seria, ya que principalmente expresa la intención en lugar de un algoritmo explícito.

Otras lecturas

Esta pregunta surge mucho ... ver, por ejemplo:

La conferencia del premio Turing de John Backus explica con gran detalle las motivaciones de la programación funcional:

¿Puede la programación liberarse del estilo von Neumann?

Realmente no debería mencionar ese artículo en el contexto actual porque se vuelve bastante técnico, bastante rápido. Simplemente no pude resistirme porque creo que es realmente fundamental.


Anexo - 2013

Los comentaristas señalan que los lenguajes contemporáneos populares ofrecen otros estilos de programación además de procedimental y funcional. Estos lenguajes suelen ofrecer uno o más de los siguientes estilos de programación:

  • consulta (p. ej. listas por comprensión, consulta integrada en el idioma)
  • flujo de datos (por ejemplo, iteración implícita, operaciones masivas)
  • orientado a objetos (por ejemplo, datos y métodos encapsulados)
  • orientado al lenguaje (por ejemplo, sintaxis específica de la aplicación, macros)

Consulte los comentarios a continuación para ver ejemplos de cómo los ejemplos de pseudocódigo de esta respuesta pueden beneficiarse de algunas de las funciones disponibles en esos otros estilos. En particular, el ejemplo de procedimiento se beneficiará de la aplicación de prácticamente cualquier construcción de nivel superior.

Los ejemplos exhibidos evitan deliberadamente mezclar estos otros estilos de programación para enfatizar la distinción entre los dos estilos en discusión.

Alcance
fuente
1
De hecho, una buena respuesta, pero ¿podría simplificar un poco el código, por ejemplo: "función allOdd (palabras) {foreach (palabra automática en palabras) {impar (longitud (palabra)? Return false:;} return true;}"
Dainius
El estilo funcional allí es bastante difícil de leer comparado con el "estilo funcional" en Python: def odd_words (palabras): return [x para x en palabras si impar (len (x))]
recuadro
@boxed: su odd_words(words)definición hace algo diferente a la respuesta allOdd. Para el filtrado y el mapeo, a menudo se prefieren las listas por comprensión, pero aquí allOddse supone que la función reduce una lista de palabras a un solo valor booleano.
ShinNoNoir
@WReach: Habría escrito su ejemplo funcional así: función allOdd (palabras) {retorno y (impar (longitud (primeras (palabras))), allOdd (resto (palabras))); } No es más elegante que su ejemplo, pero en un lenguaje recursivo de cola tendría las mismas características de rendimiento que el estilo imperativo.
Mishoo
@mishoo El lenguaje debe ser tanto recursivo como estricto y cortocircuitado en el operador y para que su suposición se mantenga, creo.
kqr
46

La diferencia real entre la programación funcional e imperativa es la forma de pensar: los programadores imperativos piensan en variables y bloques de memoria, mientras que los programadores funcionales piensan: "¿Cómo puedo transformar mis datos de entrada en datos de salida?", Su "programa" es la tubería. y conjunto de transformaciones en los datos para llevarlos de la Entrada a la Salida. Esa es la parte interesante en mi opinión, no el bit "No usarás variables".

Como consecuencia de esta mentalidad, los programas de planificación familiar suelen describir lo que sucederá, en lugar del mecanismo específico de cómo sucederá; esto es poderoso porque si podemos establecer claramente lo que significa "Seleccionar", "Dónde" y "Agregar", son libres de intercambiar sus implementaciones, al igual que hacemos con AsParallel () y, de repente, nuestra aplicación de un solo subproceso se escala a n núcleos.

Ana Betts
fuente
¿De alguna manera puede contrastar los dos usando fragmentos de código de ejemplo? realmente lo aprecio
Philoxopher
1
@KerxPhilo: Aquí hay una tarea muy simple (sume números del 1 al n). Imperativo: modificar el número actual, modificar la suma hasta ahora. Código: int i, sum; suma = 0; para (i = 1; i <= n; i ++) {suma + = i; }. Funcional (Haskell): tome una lista perezosa de números, dóblelos mientras suma cero. Código: foldl (+) 0 [1..n]. Lo sentimos, no hay formato en los comentarios.
dirkt
+1 a la respuesta. En otras palabras, la programación funcional consiste en escribir funciones sin efectos secundarios siempre que sea posible, es decir, la función siempre devuelve lo mismo cuando se le dan los mismos parámetros, esa es la base. Si sigue ese enfoque al extremo, sus efectos secundarios (siempre los necesita) se aislarán y el resto de las funciones simplemente transformarán los datos de entrada en datos de salida.
beluchin
12
     Isn't that the same exact case for procedural programming?

No, porque el código de procedimiento puede tener efectos secundarios. Por ejemplo, puede almacenar el estado entre llamadas.

Dicho esto, es posible escribir código que satisfaga esta restricción en lenguajes considerados de procedimiento. Y también es posible escribir código que rompa esta restricción en algunos lenguajes considerados funcionales.

Andy Thomas
fuente
1
¿Puede mostrar un ejemplo y una comparación? Realmente lo agradecería si pudiera.
Philoxopher
8
La función rand () en C proporciona un resultado diferente para cada llamada. Almacena el estado entre llamadas. No es referencialmente transparente. En comparación, std :: max (a, b) en C ++ siempre devolverá el mismo resultado dados los mismos argumentos, y no tiene efectos secundarios (que yo sepa ...).
Andy Thomas
11

No estoy de acuerdo con la respuesta de WReach. Deconstruyamos un poco su respuesta para ver de dónde viene el desacuerdo.

Primero, su código:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

y

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Lo primero que hay que tener en cuenta es que está fusionando:

  • Funcional
  • Orientado a la expresión y
  • Centrado en iteradores

programación, y falta la capacidad de la programación de estilo iterativo para tener un flujo de control más explícito que un estilo funcional típico.

Hablemos rápidamente de estos.

El estilo centrado en la expresión es aquel en el que las cosas, en la medida de lo posible, se evalúan . Aunque los lenguajes funcionales son famosos por su amor por las expresiones, en realidad es posible tener un lenguaje funcional sin expresiones componibles. Voy a inventarme uno, donde no hay expresiones, solo declaraciones.

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

Esto es más o menos lo mismo que se dio antes, excepto que las funciones están encadenadas puramente a través de cadenas de declaraciones y enlaces.

Un estilo de programación centrado en el iterador podría ser uno de Python. Usemos un estilo puramente iterativo y centrado en el iterador:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

Esto no es funcional, porque cada cláusula es un proceso iterativo y están unidas por una pausa explícita y la reanudación de los marcos de pila. La sintaxis puede estar inspirada parcialmente en un lenguaje funcional, pero se aplica a una encarnación completamente iterativa del mismo.

Por supuesto, puedes comprimir esto:

def all_odd(words):
    return all(odd(len(word)) for word in words)

Imperative no se ve tan mal ahora, ¿eh? :)

El punto final fue sobre un flujo de control más explícito. Reescribamos el código original para hacer uso de esto:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

Usando iteradores puede tener:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Entonces, ¿de qué sirve un lenguaje funcional si la diferencia está entre:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


La principal característica definitiva de un lenguaje de programación funcional es que elimina la mutación como parte del modelo de programación típico. La gente suele pensar que esto significa que un lenguaje de programación funcional no tiene declaraciones o utiliza expresiones, pero estas son simplificaciones. Un lenguaje funcional reemplaza el cálculo explícito con una declaración de comportamiento, sobre la cual el lenguaje luego realiza una reducción.

Restringirse a este subconjunto de funcionalidades le permite tener más garantías sobre el comportamiento de sus programas y esto le permite componerlos con mayor libertad.

Cuando tiene un lenguaje funcional, crear nuevas funciones generalmente es tan simple como componer funciones estrechamente relacionadas.

all = partial(apply, and)

Esto no es simple, o quizás no sea posible, si no ha controlado explícitamente las dependencias globales de una función. La mejor característica de la programación funcional es que puede crear consistentemente abstracciones más genéricas y confiar en que pueden combinarse en un todo mayor.

Veedrac
fuente
Sabes, estoy bastante seguro de que an applyno es la misma operación que un foldo reduce, aunque estoy de acuerdo con la buena capacidad de tener algoritmos muy genéricos.
Benedict Lee
Nunca he oído hablar de applysignificar foldo reduce, pero me parece que tiene que estar en este contexto para que devuelva un booleano.
Veedrac
Ah, está bien, me confundí con el nombre. Gracias por aclararme.
Benedict Lee
6

En el paradigma procedimental (¿debería decir "programación estructurada" en su lugar?), Ha compartido memoria e instrucciones mutables que la leen / escriben en alguna secuencia (una tras otra).

En el paradigma funcional, tienes variables y funciones (en el sentido matemático: las variables no varían con el tiempo, las funciones solo pueden calcular algo en función de sus entradas).

(Esto está muy simplificado, por ejemplo, los FPL generalmente tienen facilidades para trabajar con memoria mutable, mientras que los lenguajes de procedimiento a menudo pueden admitir procedimientos de orden superior, por lo que las cosas no son tan claras; pero esto debería darle una idea)

Artyom Shalkhakov
fuente
2

The Charming Python: La programación funcional en Python de IBM Developerworks realmente me ayudó a comprender la diferencia.

Especialmente para alguien que conoce un poco Python, los ejemplos de código de este artículo en los que se contrastan diferentes funciones de manera funcional y procedimental, pueden aclarar la diferencia entre programación procedimental y funcional.

Abbafei
fuente
2

En la programación funcional, para razonar sobre el significado de un símbolo (variable o nombre de función), solo necesita saber 2 cosas: el alcance actual y el nombre del símbolo. Si tiene un lenguaje puramente funcional con inmutabilidad, ambos son conceptos "estáticos" (perdón por el nombre sobrecargado), lo que significa que puede ver ambos, el alcance actual y el nombre, con solo mirar el código fuente.

En la programación de procedimientos, si desea responder a la pregunta cuál es el valor detrás x, también debe saber cómo llegó allí, el alcance y el nombre por sí solos no son suficientes. Y esto es lo que yo vería como el mayor desafío porque esta ruta de ejecución es una propiedad de "tiempo de ejecución" y puede depender de tantas cosas diferentes, que la mayoría de la gente aprende a depurarla y no a intentar recuperar la ruta de ejecución.

vasily
fuente
1

Recientemente he estado pensando en la diferencia en términos del problema de expresión . La descripción de Phil Wadler se cita con frecuencia, pero la respuesta aceptada a esta pregunta probablemente sea más fácil de seguir. Básicamente, parece que los lenguajes imperativos tienden a elegir un enfoque del problema, mientras que los lenguajes funcionales tienden a elegir el otro.

Juan L
fuente
0

Una diferencia clara entre los dos paradigmas de programación es el estado.

En la programación funcional, se evita el estado. En pocas palabras, no habrá ninguna variable a la que se le asigne un valor.

Ejemplo:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Sin embargo, la programación por procedimientos usa state.

Ejemplo:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
Tox
fuente