Lenguaje de programación funcional: Introducción a los lenguajes menores LISP / Scheme

1. Información general

Desde que el profesor Qiu Zongyan tradujo " Estructura e interpretación de programas informáticos " (Estructura e interpretación de programas informáticos, SICP después) la segunda edición, este libro de texto introductorio de programación informática del MIT comenzó a prestar cada vez más atención a los desarrolladores chinos. Al mismo tiempo, también le preocupa la programación funcional (Programación funcional) que introduce y el lenguaje Scheme utilizado en los ejemplos.

Hace 30 años atrás, en 1975, Bill Gates y Paul Allen escribieron la legendaria versión BÁSICA; luego la vendieron a MITS a cambio de la versión BÁSICA de la "primera olla de oro". Ese mismo año, Gerald Sussman, autor de SICP, inventó el lenguaje Scheme. De hecho, Scheme no es un idioma nuevo, para ser precisos, es solo una variante y un dialecto de LISP. Ya en 1958, John McCarthy comenzó a estudiar un lenguaje "utilizado para procesar datos de listas" ; este es también el origen del nombre LISP (procesamiento LISt). El " procesamiento de listas" a primera vista es un problema bastante específico, pero de hecho este tipo de problema tiene connotaciones importantes y de gran alcance, y veremos la historia aquí más adelante.

 

2. Introducción a las características y ejemplos del lenguaje Scheme

En comparación con otros dialectos de LISP, la característica más importante de Scheme puede ser que se puede compilar en código de máquina. En otras palabras, funciona de manera más eficiente. Además, se puede decir que Scheme es bastante satisfactorio en términos de lenguaje. La filosofía que LISP siempre ha admirado es " micro core + alta escalabilidad ", y Scheme también ha llevado esta característica al extremo. Las palabras clave integradas en el esquema (palabras clave) son lamentablemente pocas, incluso operaciones como mayor que menor que, suma, resta, multiplicación y división aparecen en forma de funciones. Incluso puede ser una exageración decir que siempre que haya palabras clave definidas y paréntesis, todos los programas se pueden escribir. Sin embargo, un efecto secundario de este estilo es que habrá muchos paréntesis en el programa, por lo que algunas personas llaman en broma a LISP "Muchos paréntesis irritantes y espurios" (Muchos paréntesis irritantes y espurios). Por ejemplo, el siguiente programa se usa para elevar al cuadrado un valor:

(definir (cuadrado x)

      (* xx))

(pantalla (cuadrado 3))

Para aquellos de nosotros que comenzamos con el lenguaje C y estamos acostumbrados a la programación procedimental (a diferencia de la "programación funcional"), cuando entramos en contacto por primera vez con LISP / Scheme, el primer toque fue probablemente: Scheme no distingue entre datos y operación . Aún tomando el ejemplo de "cuadrar", "cuadrado de x" se puede expresar como "usando 1 como base y multiplicando por x" dos veces ". Si usa el lenguaje C ++, esta lógica se puede implementar así:

int cuadrado (int x) {

      return 1 * x * x;

}

En Scheme, también podemos lograr esto:

(definir (dos veces func base arg)

      (func base (func base arg)))

(definir (cuadrado x)

      (dos veces * 1 x))

¿Cuáles son las características de esta realización? La característica más importante es que una operación (operación de multiplicación) se pasa como parámetro. De acuerdo con la "palabra negra" del diseño del programa, si una unidad de programa puede pasarse como parámetro y valor de retorno, esta unidad se denomina " primera clase " (primera clase) . En lenguajes como C / C ++ / Java, aunque también es posible pasar "operaciones" en forma de punteros de función, functores, etc., está empaquetado después de todo. En Scheme , otra función se puede pasar directamente como un parámetro a la función, y se puede pasar a otra función como un valor de retorno, y la función (es decir, la "operación") se trata completamente como un ciudadano de primera clase .

¿Cuáles son los beneficios de esto? En el ejemplo anterior, abstraemos la lógica de "ejecutar una operación dos veces" y obtuvimos la función doble. Si queremos lograr "hacer la operación de suma dos veces con 0 como base" (es decir, "multiplicar por 2"), solo necesitamos escribir:

(definir (doble x)

      (dos veces + 0 x))

La función doble aquí es "una función que opera en otras funciones", y su resultado depende de qué función se pasa como parámetro. Estas "funciones de funciones" se denominan "funciones de orden superior" en términos de programación funcional. La capacidad de implementar de forma natural funciones de orden superior es la segunda característica importante de Scheme. Como se mencionó anteriormente, el nombre LISP significa "procesamiento de listas" De hecho, la capacidad de procesar datos de listas proviene del uso de funciones de orden superior. Por ejemplo, tenemos una lista como esta:

{1, 2, 3, 4, 5}

Para esta lista, necesitamos hacer dos cosas:

1. Duplique cada elemento para obtener una nueva lista: {2, 4, 6, 8, 10}

2. Cuadrar cada elemento para obtener una nueva lista: {1, 4, 9, 16, 25}

Usando el lenguaje Java, podemos lograr esto:

Lista <int> doubleList (Lista <int> src) {

      List <int> dist = new ArrayList <int> ();

      para (int item: src) {

            dist.add (elemento * 2);

}

return dist;

}

List <int> squareList (List <int> src) {

      List <int> dist = new ArrayList <int> ();

      para (int item: src) {

            dist.add (elemento * elemento);

}

return dist;

}

El problema es claro de un vistazo: a excepción de las dos líneas de código en negrita, estos dos métodos están casi completamente duplicados. Cuando lo piensa, estos dos métodos en realidad hacen cosas muy similares: recorrer una lista y asignar cada elemento de la lista original a la nueva lista de acuerdo con una "cierta regla". Debido a que esta "cierta regla" es una operación de mapeo de elementos, para abstraer esta operación de lista, debemos implementar una función de orden superior para pasar la operación de mapeo real en forma de parámetros. Por lo tanto, en Scheme que puede implementar fácilmente funciones de orden superior, la lógica anterior es bastante fácil de implementar:

(define (src de doble lista)

      (mapa doble src))

(definir (src de lista cuadrada)

      (mapa cuadrado src))

Debido al poder y la conveniencia de las funciones de orden superior en términos de abstracción de la lógica operativa, muchas personas han comenzado a tratar de tratar las operaciones como ciudadanos de primera clase en lenguajes procedimentales "dominantes" y luego realizar funciones de orden superior. Por ejemplo, en forma de delegado, C # permite que los métodos se pasen como parámetros o valores de retorno, y las operaciones de alto nivel como encontrar se agregan a List <T>; FunctionalJ en el mundo Java ( http: //functionalj.sourceforge .net / ) Sobre la base de los genéricos proporcionados por Java5, se proporcionan operaciones de lista de uso común, como filtro y mapa. Si el ejemplo anterior se implementa con FunctionalJ, se puede escribir como:

// doble y cuadrado son instancias de función

Lista <int> doubleList (Lista <int> src) {

      return Functions.map (doble, src);

}

List <int> squareList (List <int> src) {

return Functions.map (cuadrado, src);

}

 

3. Ventajas y desventajas

La frase "no distingue entre datos y operación" es sencilla de decir, pero en realidad hay una cuestión filosófica importante detrás de ella, es decir, la cuestión de "qué es el tiempo". Según el concepto de programación procedimental, "tiempo" es la operación de una variable interna, y el programa registra el estado instantáneo del sistema en varios puntos en el tiempo en forma de variables locales; y según el concepto de programación funcional, "tiempo" es la operación de variables externas. La función se pasa como un parámetro, y no hay un estado local dentro de la función, y no hay una operación de asignación. O para decirlo de manera más simple, llamar a la misma función con los mismos parámetros en cualquier momento definitivamente obtendrá el mismo resultado. Esta propiedad se denomina " transparencia referencial" . Si la operación no tiene transparencia referencial, no se puede pasar como parámetro o valor de retorno, porque el entorno y la secuencia de la llamada pueden cambiar los resultados de funciones de orden superior.

Los programas con transparencia referencial tienen un beneficio adicional: son inherentemente seguros para subprocesos . No importa cuántos subprocesos haya y en qué orden se acceda, siempre que el programa sea referencialmente transparente, no se necesita ningún mecanismo de sincronización de subprocesos adicional para garantizar los resultados correctos. Esto es particularmente importante en las aplicaciones del lado del servidor, especialmente las aplicaciones web, que enfrentan muchos usuarios al mismo tiempo. Rod Johnson aboga por las "aplicaciones del lado del servidor Java sin estado" en su libro "Desarrollo J2EE sin EJB", y los desarrolladores de aplicaciones empresariales también se benefician mucho de la idea de la programación funcional.

Se dice que la invención original de LISP fue bastante involuntaria: McCarthy solo implementó una gramática abstracta basada en operaciones lambda y la tiró a un lado, pero sus estudiantes encontraron que escribir programas en una gramática tan minimalista se divirtió mucho. Algunas personas dicen que los informáticos son un grupo de personas a las que les gusta la reducción, pero la invención de LISP demuestra desde un punto de vista práctico: básicamente, todas las estructuras de programas se pueden reducir a operaciones lambda. Según la teoría del cálculo de Church inventada por Alonzo Church, todas las funciones que se pueden calcular de manera eficiente, incluidas las funciones de valor fijo, se pueden definir mediante operaciones lambda. Por ejemplo, los datos "0" y la operación "más 1" se pueden definir mediante operaciones lambda de la siguiente manera:

(definir cero (lambda (f) (lambda (x) x)))

(definir (agregar-1 n)
  (lambda (f) (lambda (x) (f ((nf) x)))))

Sobre esta base, las operaciones lambda se pueden utilizar para definir todo el sistema de números naturales. Este es un ejemplo extremo. En muchos otros lugares, LISP / Scheme también puede analizar los conceptos a los que estamos acostumbrados de manera similar, lo que nos permite obtener conocimientos más profundos. Por ejemplo, en el segundo capítulo de SICP " estructural Abstracción de datos ", que vimos con nuestros propios ojos: el habitual " de programación orientado al procedimiento " y " programación orientada a objetos ", en gran medida, no son más que diferentes azúcares sintácticas utilizando el mismo conjunto de operaciones lambda. Eso es todo . Cuando los programadores que están familiarizados con el esquema de aprendizaje orientado a objetos, a menudo obtienen nuevos conocimientos porque cierran la brecha entre "datos" y "operación". Además, la sintaxis de Scheme es extremadamente simple, y las palabras clave más comúnmente utilizadas probablemente no sean más de 5, por lo que tiene una ventaja única como lenguaje de enseñanza: muchas escuelas usan Java como lenguaje de programación para estudiantes universitarios. a estos pobres estudiantes Dos meses después, cuando todavía estás enredado con una sintaxis extraña como "clase interna anónima" y una biblioteca complicada como "IO stream decorator", no es difícil para ti entender lo que quiero decir.

Pero esta simplicidad también se ha convertido en el mayor obstáculo para la promoción de Scheme en las aplicaciones empresariales: las aplicaciones empresariales no requieren muchas posibilidades elegantes, sino una solución viable. Aunque las versiones de implementación de Scheme como PLT proporcionan bibliotecas de herramientas como XML y servlet, la sintaxis demasiado flexible, la falta de mejores prácticas y la falta de soporte de los grandes proveedores han hecho que Scheme finalmente no pueda convertirse en la corriente principal de las aplicaciones empresariales. Sin embargo, aunque hay pocas aplicaciones reales, las ideas de la programación funcional aún inspiran a los desarrolladores de aplicaciones empresariales. Por ejemplo, la función de continuación introducida por WebWork2.2 se deriva del concepto de programación funcional.

Por último, pero no menos importante, debe decirse: aunque rara vez se ve en aplicaciones empresariales, LISP / Scheme se usa ampliamente en computación científica, inteligencia artificial, modelado matemático y otros campos, por lo que se llama Es algo injusto ser un "idioma menor". En general, LISP / Scheme es mejor que escribir lógica algorítmica, pero no es bueno para las operaciones de E / S. Por supuesto, podemos decir que esta es la razón por la que se frustra en el campo de las aplicaciones empresariales, pero ¿por qué no puede ser el resultado de ello?

Supongo que te gusta

Origin blog.csdn.net/smilejiasmile/article/details/107644992
Recomendado
Clasificación