Lenguajes y Paradigmas de Programación
 

Tema 2: Características de la programación funcional

Aproximación teórica a la programación funcional

Programación declarativa

El paradigma de programación funcional comparte, junto con el de programación lógica, características de programación declarativa.

La característica fundamental del paradigma declarativo es que no existe la asignación ni el cambio de estado en un programa. Las variables son identificadores de valores que no cambian en toda la evaluación (como constantes definidas con un DEFINE de C). Sólo existen valores y expresiones matemáticas que devuelven nuevos valores a partir de los declarados.

En los lenguajes imperativos, sin embargo, se realizan asignaciones que cambian el valor de una variable ya existente.

Consideremos el siguiente ejemplo:

1. { int x = 1;
2.   x = x+1;
3.   int y = x+1;
4.   { int x = y;
5.     y = x+2; }
6.   y = x;}

En este fragmento de programa se mezclan instrucciones imperativas con instrucciones declarativas. Por ejemplo, las instrucciones 1, 3 y 4 son declarativas, ya que están definiendo una variable con un valor asociado (están dando un nombre a un valor). Sin embargo, las sentencias 2, 5 y 6 son imperativas, ya que están modificando el valor de una variable mediante asignaciones de nuevos valores.

Otro elemento interesante del ejemplo es el ámbito de alcance (scope en inglés) de las declaraciones de las variables. Por ejemplo, la variable x declarada en la línea 4 tiene un ámbito distinto de la declarada en la línea 1. La x de la línea 5 es la declarada en la línea 4, mientras que la x de la línea 6 es la declarada en la línea 1. Dentro de un mismo ámbito podemos renombrar todas las ocurrencias de una variable sin que el programa cambie. Por ejemplo, el siguiente programa es equivalente al anterior:

1. { int x = 1;
2.   x = x+1;
3.   int y = x+1;
4.   { int z = y;
5.     y = z+2;}
6.   y = x;}
Atención, pregunta
¿Cuál es el ámbito de la variable y? Si renombráramos y por w, ¿qué ocurrencias de y tendríamos que cambiar?

Volviendo al asunto principal que nos concierne, los programas declarativos frente a los imperativos, para saber si un lenguaje es declarativo basta con comprobar el siguiente test: dentro del ámbito de declaración de las variables x1 ... xn todas las ocurrencias de una expresión e que contiene únicamente las variables x1 ... xn tienen el mismo valor.

Como consecuencia, los lenguajes declarativos tienen una interesante propiedad de optimización: si una expresión e aparece en varios lugares dentro de un mismo ámbito, sólo es necesario evaluarla una vez. Veamos por ejemplo, el siguiente programa:

(define (f x)
   ...)
(+ (f 2) (f 2))

La expresión (f 2) se utiliza dos veces para sumar el resultado. En un paradigma declarativo esta expresión no va a cambiar de valor y va a devolver siempre el mismo valor. Por ello, es posible guardar el valor que devuelve en una nueva variable y sólo realizar la llamada una vez:

(define (f x)
   ...)
(define y (f 2))
(+ y y)

Una consecuencia muy importante de esta propiedad es que en un paradigma declarativo una función llamada con los mismos argumentos siempre devuelve el mismo resultado.

Se hace notar
Lo repetimos aquí, para que resalte más: en un paradigma declarativo (como por ejemplo, el paradigma funcional) una función llamada con los mismos argumentos siempre devuelve el mismo valor.

Hemos dicho que en los lenguajes declarativos las variables denotan nombres asociados a valores. En los lenguajes imperativos, sin embargo, las variables denotan referencias a valores que existen en algún lugar del ordenador (estado) y que pueden ser cambiados con sucesivas instrucciones. Este es otro elemento que distingue los lenguajes imperativos de los declarativos. En los lenguajes declarativos no existe ese concepto de valor que reside en algún lugar del ordenador al que nos referimos mediante una variable.

Por ejemplo, veamos el siguiente programa imperativo:

{ int a[3] = {1, 5, 7}
  int *b = a;
  b[0] = 3;
  int c = a[0]+a[2];}

En este programa, las variables a y b hacen referencia al mismo array (a la misma posición de memoria). De hecho, cuando en la tercera instrucción cambiamos el valor de la primera componente de b, también estamos modificando los valores a los que se refiere a, de forma que en la última instrucción c tomará el valor de 6. Al modificar b hemos modificado a ya que ambos se refieren (apuntan, si utilizamos una terminología de C) al mismo valor. Esto se denomina un efecto lateral.

Un lenguaje declarativo no contiene referencias en el sentido del ejemplo anterior. Las referencias son exclusivas de los lenguajes imperativos. El uso de referencias permite que un mismo objeto (dato, valor) sea referenciado por más de un identificador y permite efectos laterales como el que hemos visto. Un lenguaje declarativo está libre de efectos laterales.

El Lisp y el paradigma funcional

Una vez descrito el paradigma declarativo, vamos a comentar rápidamente algunas notas sobre la historia del Lisp. El Lisp es el lenguaje más importante del paradigma funcional. De él nacen una enorme variedad de dialectos, de los que Scheme es uno de los más extendidos en ámbitos académicos en la actualidad.

El origen del Lisp se remonta al año 1956, en el que John McCarthy estaba buscando una solución para programar el computador IBM 704 en los primeros proyectos de inteligencia artificial. A finales de 1958 McCarthy, ya profesor de Ingeniería Electrónica y Marvin Minsky, profesor de matemáticas, ambos en el MIT, comenzaron el MIT Artificial Intelligence Project e iniciaron la implementación del Lisp.

Entre los elementos que inspiraron el Lisp se encuentra el formalismo de programación funcional llamado cálculo lambda (ver siguiente sección).

Uno de los factores que contribuyeron más al éxito del Lisp es su carácter pragmático. No se trata de un lenguaje puramente funcional ni declarativo, sino que es posible realizar sentencias imperativas en las que se modifican el valor de posiciones de memoria a las que hacen referencia variables. También sucede lo mismo en Scheme, y lo veremos en la segunda parte de la asignatura.

El cálculo lambda

La principal base teórica del paradigma funcional es el cálculo lambda (lambda calculus en inglés) desarrollado en la década de los 30 por Alonzo Church como modelo de computación con el mismo poder computacional que una Máquina de Turing.

El cálculo lambda proporciona una notación muy simple (pero bastante críptica al mismo tiempo) de definición de funciones matemáticas. Sólo son necesarias tres reglas sintácticas para definir las expresiones del cálculo lambda:

expresión-lambda ::= variable
                    | constante
                    | abstracción
                    | aplicación

abstracción ::= lambda variable.expresión-lambda

aplicación ::= (expresión-lambda)expresión-lambda

La abstracción sirve para construir nuevas funciones matemáticas en base a expresiones previamente definidas. La aplicación, como su nombre indica, denota la aplicación de una función. Veamos algunos ejemplos simplificados (necesitaríamos algo más de tiempo para explicar la versión real).

Una función es una construcción matemática que acepta una entrada y produce una salida. Supongamos que tenemos una función "pintado-amarillo" que produce las siguientes salidas a partir de las correspondientes entradas:

cuadrado -> pintado-amarillo cuadrado
coche -> pintado-amarillo coche
mickey-mouse -> pintado-amarillo mickey-mouse

Podemos usar el cálculo lambda para describir esta función:

lambda x.pintado-amarillo x

Esto es lo que se denomina una expresión-lambda.

Si queremos aplicar la función a un argumento, aplicamos la siguiente sintaxis:

(lambda x.pintado-amarillo x)cuadrado -> pintado-amarillo cuadrado

El resultado de aplicar una expresión-lambda también puede ser una función, como en el siguiente ejemplo en el definimos un "constructor de funciones coloreadoras":

lambda y.lambda x.pintado-y x

Podemos usar esto para crear una función que pinta de color verde:

(lambda y.lambda x.pintado-y x)verde -> lambda x.pintado-verde x

Las funciones también pueden ser argumentos de otras funciones, como esta función "aplicar-a-mickey-mouse":

lambda f.(f)mickey-mouse

Podemos entonces llamar a esta función con la función "pintado-amarillo" para pintar a mickey-mouse de amarillo:

(lambda f.(f)mickey-mouse)lambda x.pintado-amarillo x
-> (lambda x.pintado-amarillo x)mickey-mouse
-> pintado-amarillo mickey-mouse

La evaluación de las expresiones-lambda se basa en dos reglas de sustitución de argumentos formales por los argumentos reales, denominadas reducción-alfa y reducción-beta. No vamos a verlo en detalle, sólo comentar la regla de reducción-beta:

La aplicación de una expresión-lambda a un argumento se consigue reemplazando en el cuerpo de la expresión-lambda su variable ligada por el argumento:

(lambda x.P)Q -> [Q/x]P

donde [Q/x]P significa la substitución de x por Q en cualquier ocurrencia libre de x en P.

Para más información sobre el cálculo lambda: An Introduction to Lambda Calculus and Scheme y lambda calculus en la Wikipedia.

Modelo de sustitución

Durante la clase de hoy hemos estado hablando del paradigma declarativo, del paradigma funcional y hemos presentado un modelo formal que ha sido el origen del paradigma funcional. Vamos a terminar con el modelo computacional que va a explicar la semántica de los programas que escribamos dentro del paradigma funcional. Se trata del modelo de sustitución.

Un modelo computacional es un formalismo (conjunto de reglas) que definen el funcionamiento de un programa. En el caso de Scheme, y de los lenguajes funcionales basados en la evaluación de expresiones, el modelo computacional define cuál va a ser el resultado de evaluar una determinada expresión.

El modelo de sustitución se basa en una versión simplificada de la regla de reducción del cálculo lambda.

Reglas para evaluar una expresión e de Scheme:

  1. Si e es un valor primitivo, devolver ese mismo valor.
  2. Si e es una variable, devolver su valor asociado.
  3. Si e es una expresión del tipo (f arg1 ... argn), donde f el nombre de un procedimiento primitivo ('+', '-', ...), evaluar arg1 ... argn y aplicar el procedimiento al resultado.
  4. Si e es una expresión del tipo (f arg1 ... argn), donde f el nombre de un procedimiento compuesto (definido con un 'define'), sustituir f por su cuerpo, reemplazando cada parámetro formal del procedimiento por el correspondiente argumento evaluado. Evaluar la expresión resultante.

Para ilustrar estas regla, vamos a definir unas funciones:

(define (double x) (+ x x))
(define (square y) (* y y))
(define (f z) (+ square (double z)) 1))

Y ahora vamos a ver qué ocurre cuando ejecutamos (f (+ 2 1)) utilizando el modelo de sustitución:

  1. Evaluamos la expresión: f es un procedimiento; ¿qué es (+ 2 1)? --> (f 3))
  2. Ahora tomamos el cuerpo de f y sustituimos 3 por z: (+ square (double 3)) 1)
  3. Ahora evaluamos la expresión resultante: + denota el procedimiento suma, 1 se evalúa a 1. ¿Qué es (square (double 3))? square es un procedimiento compuesto; y ¿qué es (double 3)? double es otro procedimiento compuesto y 3 se evalúa a 3. Así que, una vez hemos evaluado todo, podemos empezar a aplicar los procedimientos. Empezamos por double sustituyendo el cuerpo de la función: (+ (square (+ 3 3)) 1)
  4. Ahora, ¿qué es (+ 3 3)? --> (+ (square 6) 1)
  5. Ahora se sustituye el cuerpo de square: (+ (* 6 6) 1)
  6. ¿Qué es (* 6 6)? --> (+ 36 1)
  7. ¿Y (+ 36 1)? --> 37

Orden de evaluación normal vs. de aplicación

El ejemplo del modelo de sustitución que hemos visto se ha hecho utilizando el "orden de aplicación", donde se evalúa primero todos los procedimientos y argumentos antes de ejecutar la llamada a la función.

Otra forma de evaluar expresiones es utilizando el "orden normal": expandir completamente los procedimientos hasta que todo está expresado mediante operaciones primitivas y autoevaluaciones, y entonces evaluar la expresión.

El mismo problema de antes: (f (+ 2 1))

  1. Expandimos f, dejando (+ 2 1) sin evaluar: (+ (square (double (+ 2 1))) 1)
  2. Ahora expandimos square: (+ (* (double (+ 2 1)) (double (+ 2 1))) 1)
  3. Y ahora expandimos double: (+ (* (+ (+ 2 1) (+ 2 1)) (+ 2 1) (+ 2 1))) 1)
  4. Por último, expandimos todo los operadores primitivos y autoevaluamos los valores. Se hace uno por uno
    (+ (* (+ 3 (+ 2 1)) (+ (+ 2 1) (+ 2 1))) 1)
    (+ (* (+ 3 3) (+ (+ 2 1) (+ 2 1))) 1)
    (+ (* (+ 3 3) (+ 3 (+ 2 1))) 1)
    (+ (* (+ 3 3) (+ 3 3))) 1)
    (+ (* 6 (+ 3 3)) 1)
    (+ (* 6 6) 1)
    (+ 36 1)
    37
    

Hemos visto las dos formas de evaluación: con el orden aplicativo, evaluamos completamente todos los argumentos antes de aplicar el procedimiento. Mientras que en el orden normal, primero expandimos completamente los procedimientos hasta que todo está expresado mediante operaciones primitivas y valores. Entonces, en uno y en otro, podemos encontrar la solución de la expresión.

Una consideración muy importante es que en programación funcional, donde una función siempre devuelve el mismo valor cuando es llamada con los mismos argumentos, el orden normal y el orden aplicativo siempre devolverán el mismo resultado. Sin embargo, esto no sucede cuando estamos en un lenguaje no funcional. Veamos el siguiente ejemplo:

Vamos a definir una función que debería devolver siempre 0:

(define (zero x) (- x x))        ; Esta función siempre
                                         ; debería devolver 0.

Ahora vamos a evaluar (zero (random 10)) con orden aplicativo y con orden normal.

Orden aplicativo:

(zero (random 10))
   (random 10) ==> 5
(zero 5) ----> 
(- 5 5) ==> 0
0                               ; El orden de aplicación devuelve 0 

Orden normal:

(zero (random 10))
(zero (random 10)) ----> 
(- (random 10) (random 10))
   (random 10) ==> 4
   (random 10) ==> 8
(- 4 8) ==> -4
-4                              ; El orden normal no

La regla es que si estás haciendo programación funcional, obtienes las mismas respuestas sea cual sea el orden de evaluación. ¿Por qué no sucede así en el caso de (zero (random 10))? Porque no es una función pura, ya que puede devolver valores distintos en distintas invocaciones.

Otra diferencia entre la evaluación normal y la evaluación aplicativa es la eficiencia. Veamos un ejemplo. Intenta computar

(square (square (+ 2 3)))

en orden normal y de evaluación. En este caso el orden de aplicación es más eficiente proque sólo suma 2 y 3 una vez, no cuatro. (Pero más adelante, veremos que esto no es una regla general, a veces el orden normal es más eficiente).

Aproximación práctica a la PF

Características del paradigma funcional

Uno de los primeros modelos computacionales, el cálculo lambda (Alonzo Church, 1930), está basado completamente en un enfoque funcional, en la creación y uso de funciones matemáticas. Este modelo computacional estableció las bases teóricas de lo que más tarde serían los lenguajes de programación funcionales.

La popularidad del paradigma funcional se ha debido en gran medida al éxito del lenguaje de programación Lisp (y de sus dialectos, como Scheme). Tanto el Lisp como Scheme son lenguajes que se están utilizando en la actualidad (como ejemplo de uso del Lisp como lenguaje de programación de proyectos reales, leer la charla de Paul Graham titulada Beating the Averages, en la que explica cómo utilizó el Lisp para construir Viaweb, una de las primeras tiendas on-line adquirida en 1998 por Yahoo).

Hay que resaltar que tanto Lisp como Scheme no son lenguajes funcionales puros (a diferencia de otros lenguajes como Haskell). En estas primeras clases vamos a usar únicamente las carracterísticas funcionales de Scheme. Dejaremos para más adelante sus construcciones imperativas.

Entre las características del paradigma funcional destacamos:

  • Programación declarativa
  • Definición y evaluación de funciones
  • Uso de la recursión
  • Funciones como datos primitivos

Ya tratamos en la clase anterior algunos conceptos sobre programación declarativa, en la clase de hoy vamos a tratar las otras tres características. Lo vamos a hacer desde un punto de vista bastante práctico, con númerosos ejemplos en Scheme.

Definición y evaluación de funciones

Cualquier lenguaje funcional permite la definición de nuevas funciones como forma de abstracción, y la evaluación de expresiones como forma de computación.

Ya hemos visto cómo definir y evaluar funciones en Scheme:

(define (cuadrado x)
   (* x x))

(+ (cuadrado 3) (cuadrado 2))

Estamos definiendo una función con un argumento formal (x) que tiene como cuerpo la expresión (* x x) y le estamos dando el nombre de cuadrado. Después evaluamos una expresión en la que llamamos a la función recién definida y a la función primitiva '+'.

Uso de la recursión

Otro elemento común a todos los lenguajes funcionales es el uso de la recursión para expresar funciones que en otros lenguajes se expresan con iteraciones. Muchas veces es más sencillo y natural utilizar una recursión en la definición de una función. Veremos hoy algún ejemplo sencillo. En clases posteriores analizaremos más a fondo la recursión en Scheme.

Factorial

Un primer ejemplo es la típica función factorial que calcula el factorial de un número.

Matemáticamente, el factorial de un número se puede expresar con la siguiente formulación:

Esta expresión tiene una traducción directa en Scheme:

(define (factorial x)
   (if (= x 0)
      1
      (* x (factorial (- x 1)))))

>(factorial 8)
40320
>(factorial 30)
265252859812191058636308480000000

Sumatorio

Otro ejemplo de función recursiva es el sumatorio desde un número inicial hasta un límite. Matemáticamente se expresa con la siguiente fórmula:

En Scheme:

(define (sumatorio min max)
   (if (> min max)
      0
      (+ min (sumatorio (+ 1 min) max))))

> (sumatorio 3 12)
75

Por último un ejemplo utilizando símbolos y las funciones que vimos en una clase anterior empty?, first, bf y equal?.

Recordemos qué hace cada una de estas funciones:

  • (empty? palabra): comprueba si palabra es una cadena vacía
  • (first palabra): devuelve el primer carácter de un símbolo (como símbolo a su vez)
  • (bf palabra): devuelve el resto de un símbolo, después de haber quitado su primer carácter
  • (equal? pal1 pal2): comprueba si dos símbolos son iguales

La función (elemento? x palabra) devuelve #t o #f dependiendo de si x es un símbolo que está dentro de palabra:

(define (elemento? x palabra)
   (cond 
      ((empty? palabra) #f)
      ((equal? x (first palabra)) #t)
      (else (elemento? x (bf palabra)))))
> (elemento? 'a 'hola)
#t
> (elemento? 'e 'cocodrilo)
#f

Funciones como tipo de datos primitivo

Otra característica fundamental de los lenguajes funcionales es que las funciones son consideradas un tipo de dato primitivo.

Un tipo de dato primitivo es aquel que:

  • puede ser el valor de una variable (en terminología de programación funcional: nombrado por un identificador)
  • un argumento de una función
  • el valor que devuelve una función
  • componente de una estructura de datos mayor

Por ejemplo, los números, los caracteres o las cadenas son datos primitivos en la mayor parte de lenguajes de programación. Sin embargo, resulta poco frecuente que podamos hacer todas estas cosas con una función o un procedimiento. Es una de las características más sorprendentes de los lenguajes funcionales:

  • Una variable puede tener como valor un procedimiento
  • Podemos definir una función que toma como argumentos otras funciones
  • Podemos devolver un procedimiento como resultado de una llamada a otro procedimiento
  • Podemos construir estructuras de datos que contengan procedimientos como elementos (listas de procedimientos, por ejemplo)

Vamos a ver un ejemplo de cada una de estas características en Scheme:

Una función puede ser nombrada con un identificador.

Ya hemos visto algún ejemplo curioso de esto:

(define suma +)
suma
>(suma 1 2 3)
6
(define + -)
(+ 4 2)
2
(define + suma)

Podemos comprobar en el ejemplo que los símbolos 'suma', '+' o '-' no son más que identificadores ligados a procedimientos.

En Scheme cuando definimos una función con un nombre, la relación entre la función y el nombre es la misma que la relación entre una variable y un valor. El nombre de la función es el identificador que está ligado al procedimiento.

Una función puede ser el argumento de otra función.

Por ejemplo, podemos defnir la siguiente función aplicar que toma como argumento una función f 2 argumentos y realiza la llamada a f con los 2 argumentos:

(define (aplicar f x y)
   (f x y))
> (aplicar + 2 3)
5
> (aplicar * 2 3)
6
(aplicar word 'hola 'adios)
holaadios

En el siguiente ejemplo definimos una función que toma dos procedimientos unarios (de un argumento) f y g y los aplica a un número:

(define (aplicar-2 f g x)
   (f (g x)))
(define (5+ x)
   (+ 5 x))
(define (doble x)
   (* 2 x))
> (aplicar-2 5+ doble 8)
21

Una función puede ser el valor devuelto por otra función.

Vamos a definir una función que devuelve otra función. La siguiente función hacer-suma1 define en su interior la función suma1 y la devuelve.

(define (hacer-suma1)
   (define (suma1 x)
      (+ x 1))
   suma1)
(define f (hacer-suma1))
> (f 3)
4

Vemos que el resultado de la llamada a hacer-suma1 se guarda en la variable f y después comprobamos que realmente es una función que suma 1.

Podemos complicar un poco más el ejemplo definiendo un parámetro con el número que queremos sumar en la función que devolvemos. Así, la función hacer-sumak construye otra función que suma el parámetro k al argumento x que se le pasa:

(define (hacer-sumak k)
   (define (sumak x)
      (+ x k))
   sumak)
(define g (hacer-sumak 8))
> (g 5)
13

Por último, una función puede ser parte de una estructura de datos mayor.

Veamos un ejemplo que a primera vista puede parecer algo complicado (ya que mezclamos recursión y funciones como tipos de datos primitivos). Se trata de construir una función que recorra una lista de funciones unarias (se aplican a un único argumento) y las vaya aplicando a un argumento.

Vamos a verlo paso a paso. Vamos a ver en primer lugar qué es eso de una lista de funciones unarias.

(define (2+ x)
   (+ 2 x))
(define (10* x)
   (* 10 x))
(define (cuadrado x)
   (* x x))

(define lista-funcs (list cuadrado 2+ 10*))

La lista lista-funcs es una lista que contiene funciones (no identificadores). Vamos a comprobarlo. Si miramos su primer elemento el intérprete nos dice lo siguiente:

> (car lista-funcs)
#procedure:cuadrado

Podemos probar qué ese primer elemento es un procedimiento aplicándolo a un número y viendo que devuelve su cuadrado:

>((car lista-funcs) 3)
9

Podemos ahora hacer la función aplica-funcs que recorre la lista de funciones y las aplica a un número:

(define (aplica-funcs lista x)
    (if (empty? (cdr lista))
        ((car lista) x)
        ((car lista) (aplica-funcs (cdr lista) x))))

El caso base de la recursión es el caso en que la lista de funciones tiene un único argumento (eso es lo que significa (empty? (cdr lista))). En ese caso se devuelve el resultado de aplicar esa única función al número x.

En el caso general, aplicamos la primera función al resultado de la llamada recursiva de aplicar el resto de funciones al número x.

Lambda en Scheme

Los tipos de datos primitivos se pueden manejar directamente en los lenguajes de programación, sin darles nombre (asignarlos a variables). Por ejemplo, cuando queremos llamar a una función cualquiera (como cuadrado) con el número "4" como argumento, escribimos directamente "4" y de esa forma nos referimos a ese número: "4":

(cuadrado 4)

No hace falta hacer:

(define cuatro 4)
(cuadrado cuatro)

Hemos dicho que las funciones son en Scheme tipos de datos primitivos. ¿Cumplen entonces la propiedad anterior? ¿Es posible usar una función sin darle un nombre? La respuesta es que sí, utilizando la forma especial lambda.

La sintaxis de lambda es:

(lambda (<arg1> ... <argn>) <cuerpo>)

La forma especial lambda construye una función sin nombre y la devuelve como resultado. Para definir una función son necesarios dos elementos: sus argumentos y su cuerpo. Estos dos elementos son los que se definen a continuación de la palabra lambda.

Así, por ejemplo, en la siguiente expresión:

(lambda (x) 
   (* x x))

se define una función que tiene un único argumento (x) y que tiene como cuerpo la expresión (* x x). Es la función cuadrado.

Si ejecutamos esa expresión en el intérprete de Scheme veremos que el resultado de su evaluación es un procedimiento:

>(lambda (x) (* x x))
#procedure:3:2

Con ese procedimiento que nos devuelve Scheme (el procedimiento llamado con el críptico nombre de #procedure:3:2) podemos hacer bastantes cosas. Algunos ejemplos:

  1. Podemos darle un nombre y obtener una función con nombre que después podremos usar:
    > (define cuadrado (lambda (x) (* x x)))
    > (cuadrado 3)
    9
    
  2. Pasarlo como argumento de otra función que admite otras funciones como parámetro:
    >(define (aplicar-2 f g x)
       (f (g x)))
    
    >(define (suma-5 x)
       (+ 5 x))
    
    >(aplicar-2 suma-5 (lambda (x) (* x x)) 3)
    14
    
  3. Podemos evaluarlo inmediatamente poniendo un paréntesis de apertura (recordad lo que comentamos la primera semana de clase de que los paréntesis de apertura "(" servían para lanzar funciones o formas especiales que hay a continuación de ellos):
    >((lambda (x) (* x x)) 3)
    9
    

Referencias

Para saber más de los temas que hemos tratado en esta clase puedes consultar las siguientes referencias: