5. Función¶
Una función es una “subrutina” que puede ser llamada por un código externo. Como parte del programa, la función en sí también es una pieza de código. Una función puede tener 0 o más parámetros y devolverá un resultado, que se denomina valor de retorno de la función.
En Berry, la función es un valor de primera clase. Por lo tanto, además de llamar a funciones, también puede pasar funciones como valores, por ejemplo, vincular funciones a variables, usar funciones como valores devueltos, etc.
5.1 Información básica¶
El uso de funciones incluye dos partes: definición de función y llamada.
La declaración de definición de función usa la palabra clave def
como el comienzo. La definición de la función es el proceso de
empaquetar y nombrar el código del cuerpo de la función. Este proceso
solo genera la estructura de la función y no ejecuta la función. La
función de ejecución debe usar un operador de llamada, que es un par
de paréntesis. El operador de llamada actúa sobre una expresión cuyo
resultado es un tipo de función. Los parámetros que se pasan a la
función se escriben entre paréntesis y los parámetros múltiples se
separan con comas. El resultado de la expresión de llamada es el valor
de retorno de la función.
5.1.1 Definición de funciones¶
Función con nombre¶
Una función con nombre es una función a la que se le da un nombre
cuando se define. Su declaración de definición consta de las siguientes
partes: palabra clave def
, nombre de funcion, lista que constan de 0
a múltiples parámetros y cuerpo de función, múltiples parámetros en la
lista de parámetros están separados por comas, y todos los parámetros
están escritos en un par de paréntesis. Llamamos al parámetro cuando la
función se define como Parámetros formales, y al parámetro cuando
llamamos a la función como Argumentos. La forma general de la
definición de la función es:
’def’ name ´(´ argumentos ´)´
bloque
´end’
El nombre de función nombre es un identificador; argumentos es
la lista de parámetros formales; bloque es el cuerpo de la función.
Si el cuerpo de la función es una declaración vacía, la función se
denomina “función vacía”. La declaración del valor de retorno de la
función está contenida en el cuerpo de la función. Si no hay declaración
de devolución en bloque, la función devolverá nil
por defecto.
El nombre de la función es en realidad el nombre de la variable del
objeto de la función vinculada. Si el nombre ya existe en el ámbito
actual, definir la función equivale a vincular el objeto de función a
esta variable.
El siguiente ejemplo define una función llamada add
. La función de
este ejemplo es sumar dos números y devolver el resultado.
def add(a, b)
return a + b
end
La función add
tiene dos parámetros a
y b
, y los dos
sumandos se pasan a la función a través de estos parámetros para el
cálculo. La instrucción return
devuelve el resultado del cálculo.
Una función como atributo de clase se llama método. Esta parte del contenido se explicará en el capítulo orientado a objetos.
Función anónima¶
A diferencia de las funciones con nombre, la función anónima no tiene nombre y su expresión de definición tiene la forma:
´def’ ´(´ argumentos ´)´
bloque
´end’
Se puede ver que, en comparación con las funciones con nombre, no hay un nombre de función en su definición.. La definición de una función anónima es esencialmente una expresión, que se denomina Función literal. Para usar funciones anónimas podemos vincular el valor literal de la función a una variable:
add = def (a, b)
return a + b
end
La función de este código es exactamente la misma que la función add
en la sección anterior. Se puede usar una función anónima para pasar
convenientemente el valor de la función como un valor literal. Al igual
que otros tipos de literales, los literales de función también son la
unidad de expresión más pequeña. Por lo tanto, lo que hay entre las
palabras clave def
y end
es un todo indivisible.
Función de llamada¶
Tome la función add
como ejemplo. Para llamar a esta función, debe
proporcionar dos valores y puede obtener la suma de los dos números
llamando a la función:
res = add(5, 3)
print(res) # 8
Llamamos a la función llamada (la función add
en el ejemplo) como
Función llamada, y la función que llama a la función llamada (la
función principal
en el ejemplo) como Función clave. El proceso
de llamada de función es el siguiente: Primero, el intérprete
(implícitamente) inicializará la lista de parámetros formales de la
función llamada con la lista de argumentos y, al mismo tiempo,
suspenderá la función de llamada y guardará su estado, luego creará un
entorno para la función llamada y ejecutará la función llamada.
La función finalizará su ejecución cuando encuentre la instrucción
return
y pase el valor de retorno a la función que llama. El
intérprete destruirá el entorno de la función llamada después de que
regrese la función llamada, luego restaurará el entorno de la función
que llama y continuará ejecutando la función que llama. El valor de
retorno de la función también es el resultado de la expresión de la
llamada a la función. El siguiente ejemplo define una función
cuadrado
y vincula esta función a una variable f
, y luego llama
a la función cuadrado
a través de la variable f
. Este uso es
similar a los punteros de función en lenguaje C.
def cuadrado(n)
return n * n
end
f = cuadrado
print(f(5)) # 25
Cabe señalar que el objeto de la función solo está vinculado a estas variables (consulte la sección Capitulo-3: Operador de asignación
f = cuadrado
cuadrado = nil
print(f(5)) # 25
Se puede ver que la función todavía se puede llamar normalmente después
de reasignar cuadrado
. Solo después de que el objeto de función ya
no esté vinculado a ninguna variable, se perderá y el sistema reciclará
los recursos ocupados por este tipo de objeto de función.
Desviar la llamada¶
La llamada de la función debe estar en el ámbito de la variable de función, por lo que normalmente no se puede llamar antes de que se defina la función. Para resolver este problema, puede utilizar este método para comprometer:
var func1
def func2(x)
return func1(x)
end
def func1(x)
return x * x
end
print(func2(4)) # 16
En este ejemplo, func2
llama a func1
, pero la función func1
se define después de func2
. Después de ejecutar este código, el
programa generará el resultado correcto 16
. Esta rutina utiliza el
mecanismo de que no se llamará a la función cuando se defina. Defina la
variable func1
antes de definir func2
para asegurarse de que el
símbolo func1
no se encontrará durante la compilación. Luego
definimos la función func1
después de func2
para que la función
se use para sobrescribir el valor de la variable func1
. Cuando se
llama a la función func2
en la última línea print(func2(4))
, la
variable func1
ya es la función que necesitamos, por lo que se
mostrará el resultado correcto.
Llamada recursiva¶
Con función recursiva se refiere a funciones que se llaman a sí mismas directa o indirectamente. La recursividad se refiere a una estrategia que divide el problema en subproblemas similares y luego los resuelve. Tomando el factorial como ejemplo, la definición recursiva de factorial es 0! = 1, n! = n ⋅ (n−1)!. Entonces podemos escribir la función recursiva para calcular el factorial según la definición:
def fact(n)
if n == 0
return 1
end
return n * fact(n-1)
end
Tome el factorial de 5 como ejemplo, el proceso de calcular manualmente
el factorial de 5 es: ¡5! = 5 × 4 × 3 × 2 × 1 = 120. El resultado de
llamar a la función fact
también es 120:
print(fact(5)) # 120
Para garantizar que la profundidad de la llamada recursiva sea limitada
(un nivel de recursividad demasiado profundo agotará el espacio de la
pila), la función recursiva debe tener una condición de finalización. En
fact
la declaración if
en la segunda línea de la definición de
la función se usa para detectar la condición final, y el proceso
recursivo finaliza cuando n
se calcula como 0
. La fórmula
factorial anterior no se aplica a parámetros no enteros. Ejecutar una
expresión como fact(5.1)
provocará un error de desbordamiento de
pila debido a la imposibilidad de finalizar la recursividad.
Existe otra situación, la Recurrencia indirecta
, es decir, la
función no es llamada por sí misma sino por otra función (directa o
indirectamente) llamada por ella. La recursividad indirecta generalmente
requiere el uso de técnicas de llamada de función hacia adelante. Tome
las funciones es_impar
y es_par
para calcular números pares e
impares como ejemplos:
var es_impar
def es_par(n)
if n == 0
return true
end
return es_impar(n-1)
end
def es_impar(n)
if n == 0
return false
end
return es_par(n-1)
end
Estas dos funciones se llaman entre sí. Para garantizar que este nombre
esté en el alcance cuando se llama a la función es_impar
en la línea
6, la variable es_impar
se define en la línea 1.
Llamada de función anónima¶
Si una función anónima solo se llamará una vez, la forma más fácil es llamarla cuando esté definida, por ejemplo:
res = def (a, b) return a + b end (1, 2) # 3
En esta rutina, usamos la expresión de llamada directamente después del literal de función para llamar a la función. Este uso es muy adecuado para funciones que solo se llamarán en un lugar.
También puede vincular una función anónima a una variable y llamarla:
add = def (a, b) return a + b end
res = add(1, 2) # 3
Este uso es similar a la llamada de una función con nombre, esencialmente llamando a la variable vinculada al valor de la función. Cabe señalar que es más difícil realizar llamadas recursivas a funciones anónimas, a menos que utilice técnicas de llamada de reenvío.
Parámetros formales y reales¶
La función utiliza parámetros reales para inicializar los parámetros
formales cuando se llama. En circunstancias normales, el parámetro real
y el parámetro de forma son iguales y las posiciones se corresponden
entre sí, pero Berry también permite que el parámetro real sea diferente
del parámetro formal: si el parámetro real es mayor que el parámetro
formal, el parámetro real adicional al parámetro será descartado. De
otra forma los parámetros formales restantes se inicializarán a nil
.
El proceso de paso de parámetros es similar a la operación de
asignación. Para los tipos nil
, boolean
y numéricos, el paso de
parámetros es por valor, mientras que otros tipos son por referencia.
Para el tipo de referencia de paso de escritura, como una instancia,
modificarlos en la función llamada también modificará el objeto en la
función de llamada. El siguiente ejemplo demuestra esta función:
var l = [], i = 0
def func(a, b)
a.push(1)
b ='cadena'
end
func(l, i)
print(l, i) # [1] 0
Se puede ver que el valor de la variable l
ha cambiado después de
llamar a la función func
, pero el valor de la variable i
no ha
cambiado.
Función con número variable de argumentos (vararg)¶
Puede definir una función para tomar cualquier número arbitrario de
argumentos e iterarlos. Por ejemplo, print()
toma cualquier cantidad
de argumentos e imprime cada uno de ellos separados por espacios. Debe
definir el último argumento como una captura de todos los argumentos
usando *
antes de su nombre.
Todos los argumentos que siguen a los argumentos formales se agrupan en
tiempo de ejecución en una instancia de list
. Si no se capturan
argumentos, la lista está vacía.
Ejemplo:
def f(a, b, *c) return size(c) end
f(1,2) # devuelve 0, c is []
f(1,2,3) # devuelve 1, c is [3]
f(1,2,3,4) # devuelve 2, c is [3,4]
Llamar a una función con un número dinámico de argumentos¶
La sintaxis de Berry solo permite llamar con un número fijo de
argumentos. Utilice la función call(f, [args])
para pasar cualquier
número de argumentos arbitrario.
Puede agregar estáticamente cualquier número de argumentos a call()
.
Si el último argumento es una lista
, se expande automáticamente a
argumentos discretos.
Ejemplo:
def f(a,b) return nil end
call(f,1) # llama a f(1)
call(f,1,2) # llama a f(1,2)
call(f,1,2,3) # llama a f(1,2,3), el último argumento es ignorado por f
call(f,1,[2,3]) # llama a f(1,2,3), el último argumento es ignorado por f
call(f,[1,2]) # llama a f(1,2)
call(f,[]) # llama a f()
Puede combinar call
y vararg. Por ejemplo, creemos una función que
actúe como print()
pero convierta todos los argumentos a mayúsculas.
Ejemplo completo:
def print_upper(*a) # toma un número arbitrario de argumentos, args es una lista
import string
for i:0..size(a)-1
if type(a[i]) == 'string'
a[i] = string.toupper(a[i])
end
end
call(print, a) # llama a print con todos los argumentos
end
print_upper("a",1,"Foo","Bar") # imprime: A 1 FOO BAR
Funciones y variables locales¶
El cuerpo de la función en sí es un ámbito, por lo que las variables definidas en la función son todas variables locales. A diferencia de los bloques directamente anidados, cada vez que se llama a una función, se asigna espacio para las variables locales. El espacio para las variables locales se asigna en la pila y la información de asignación se determina en el momento de la compilación, por lo que este proceso es muy rápido. Cuando se anidan varios niveles de alcance en una función, el intérprete asigna espacio de pila para la cadena de anidamiento de alcance con la mayoría de las variables locales, en lugar del número total de variables locales en la función.
Declaración return
¶
La declaración return
se utiliza para devolver el resultado de una
función, es decir, el valor de retorno de la función. Todas las
funciones en Berry tienen un valor de retorno, pero no puede usar
ninguna declaración return
en el cuerpo de la función. En este
momento, el intérprete generará una declaración return
predeterminada para garantizar que la función regrese return
. Hay
dos formas de escribir oraciones:
´return’
´return’ expresión
La primera forma de escribir es escribir solo la palabra clave
return
y no la expresión que se devolverá. En este caso, se devuelve
el valor nil
predeterminado. La segunda forma de escribir es seguir
la expresión expresión después de la palabra clave return
, y el
valor de la expresión se usará como valor de retorno de la función.
Cuando el programa ejecuta la declaración return
, la función que se
está ejecutando actualmente finalizará la ejecución y volverá al código
que llamó a la función para continuar ejecutándose.
Cuando se usa una palabra clave separada return
como declaración de
retorno de una función, es fácil causar ambigüedad. En ese caso se
recomienda agregar un punto y coma después de return
para evitar
errores:
def func()
return;
x = 1
end
En este ejemplo, la declaración x = 1
después de la declaración
return
no se ejecutará, por lo que es redundante. Si se evita este
tipo de código redundante, la instrucción return
suele ir seguida de
palabras clave como end
, else
o elif
. En este caso, incluso
si se usa una declaración return
por separado, no hay necesidad de
preocuparse por la ambigüedad.
Cierre (closure)¶
Conceptos básicos¶
Como se mencionó anteriormente, las funciones son el primer tipo de valor en Berry. Puede definir funciones en cualquier lugar y también puede pasar funciones como parámetros o devolver valores. Cuando se define otra función en una función, la función anidada puede acceder a las variables locales de cualquier función externa. Llamamos a las “variables locales de la función externa” utilizadas en la función la función como Variables libres. Las variables libres generalizadas también incluyen variables globales, pero no existe tal regla en Berry. El Cierre es una técnica que vincula funciones a entornos. El entorno es un mapeo que asocia cada variable libre de una función con un valor. En términos de implementación, los cierres asocian el prototipo de función con sus propias variables. Los prototipos de funciones se generan en tiempo de compilación y el entorno es un concepto de tiempo de ejecución, por lo que los cierres también se generan dinámicamente en tiempo de ejecución. Cada cierre vincula el prototipo de función al entorno cuando se genera, como se ve en el siguiente ejemplo:
def func(i) # La función externa
def foo() # La función interna (closure)
print(i)
end
foo()
end
La función interna foo
es un cierre y tiene una variable libre
i
, que es un parámetro de la función externa func
. Cuando se
genera el cierre foo
, su prototipo de función se vincula al entorno
que contiene la variable libre i
. Cuando la variable foo
sale
del alcance, el cierre se destruirá. Por lo general, la función interna
será el valor de retorno de la función externa, por ejemplo:
def func(i) # La función externa
return def () # Devuelve un cierre (función anónima)
print(i)
i = i + 1
end
end
El cierre devuelto aquí es una función anónima. Cuando la función
externa devuelve el cierre, las variables locales de la función externa
se destruirán y el cierre no podrá acceder directamente a las variables
en la función externa original. El sistema copiará el valor de la
variable libre al entorno cuando se destruya la variable libre. El ciclo
de vida de estas variables libres es el mismo que el del cierre, y solo
el cierre puede acceder a ellas. La función o el cierre devuelto no se
ejecutará automáticamente, por lo que debemos llamar al cierre devuelto
por la función func
:
f = func(0)
f()
Este código generará 0
. Si continuamos llamando al cierre f
,
obtendremos la salida 1
, 2
, 3
… Esto puede no entenderse
bien: la variable [2.198 ] se destruye después de que la función
func
regresa , y como la variable libre del cierre f
, i
se
almacenará en el entorno de cierre, por lo que cada vez que se llame a
f
, el valor de i
se sumará a 1 (definición de la función
func
línea 4).
Uso de cierres¶
Los cierres tienen muchos usos. Aquí hay algunos usos comunes:
Evaluación perezosa¶
El cierre no hace nada hasta que se llama.
Función de comunicación privada¶
Puede permitir que algunos cierres compartan variables libres, que solo son visibles para estos cierres, y se comuniquen entre funciones cambiando los valores de estas variables libres. Esto puede evitar el uso de variables externas.
Generar múltiples funciones¶
A veces es posible que necesitemos usar múltiples funciones, estas funciones pueden tener solo diferentes valores de algunas variables. Podemos implementar una función y luego usar estas diferentes variables como parámetros de función. Una mejor manera es devolver el cierre a través de una función de fábrica y usar estas variables posiblemente diferentes como variables libres del cierre, de modo que no siempre tenga que escribir esos parámetros al llamar a la función, y cualquier número de funciones similares puede ser generado.
Simular miembros privados¶
Algunos lenguajes admiten el uso de miembros privados en objetos, pero la clase de Berry no lo admite. Podemos usar las variables libres de los cierres para simular miembros privados. Este uso no es la intención original de diseñar cierres, pero hoy en día, este “mal uso” de los cierres es muy común.
Resultado de caché¶
Si hay una función que requiere mucho tiempo para ejecutarse, llevará mucho tiempo llamarla cada vez. Podemos almacenar en caché el resultado de esta función, buscarlo en el caché antes de llamar a la función y devolver el valor almacenado en caché si lo encuentra; de lo contrario, se llama a la función y se actualiza el valor almacenado en caché. Podemos usar los cierres para guardar el valor almacenado en caché para que no quede expuesto al alcance externo, y el resultado almacenado en caché se conservará (hasta que se destruya el cierre).
Vinculación de variables libres¶
Si varios cierres vinculan la misma variable libre, todos los cierres siempre compartirán esta variable libre. Por ejemplo:
def func(i) # La función externa
return [# Devuelve la lista de cierre
def () # El cierre #1
print("cierre 1 log:", i)
i = i + 1
end,
def () # El cierre #2
print("cierre 2 log:", i)
i = i + 1
end
]
end
La función func
, en este ejemplo, devuelve dos cierres a través de
una lista, y estos dos cierres comparten la variable libres i
. Si
llamamos a estos cierres:
f = func(0)
f[0]() # cierre 1 log: 0
f[1]() # cierre 2 log: 1
Como puede ver, actualizamos la variable libre i
cuando llamamos al
cierre f[0]
, y este cambio afectó el resultado de llamar al cierre
f[1]
. Esto se debe a que si varios cierres utilizan una variable
libre, solo hay una copia de la variable libre y todos los cierres
tienen una referencia a la entidad de variable libre. Por lo tanto,
cualquier modificación a la variable libre es visible para todos los
cierres que usan dicha variable.
De manera similar, antes de que se destruyan las variables locales de la función externa, modificar el valor de la variable libre también afectará el cierre:
def func()
i = 0
def foo()
print(i)
end
i = 1
return foo
end
En este ejemplo cambiamos el valor de la variable i
(que es la
variable libre del cierre foo
) de 0
a 1
antes de que regrese
la función externa func
, luego llamamos al cierre, y después el
valor de la variable libre i
cuando el paquete foo
también es
1
:
func()() # 1
Crear cierre en bucle¶
Al construir un cierre en el cuerpo del ciclo, es posible que no desee
que las variables libres del cierre cambien con las variables del ciclo.
Primero veamos un ejemplo de cómo crear un cierre en un bucle while
:
def func()
l = [] i = 0
while i <= 2
l.push(def () print(i) end)
i = i + 1
end
return l
end
En este ejemplo, construimos un cierre en un ciclo y colocamos este
cierre en una lista
. Obviamente, cuando finalice el ciclo, el valor
de la variable i
será 3
, y todos los cierres de la lista l
también son referencias usando esta variable. Si ejecutamos el cierre
devuelto por func
obtendremos el mismo resultado:
res = func()
res[0]() # 3
res[1]() # 3
res[2]() # 3
Si queremos que cada cierre se refiera a diferentes variables libres, podemos definir otra capa de funciones y luego vincular las variables del ciclo actual con los parámetros de la función:
def func()
l = [] i = 0
while i <= 2
l.push(def (n)
return def () print(n) end
end (i))
i = i + 1
end
return l
end
Para ayudar a entender este código aparentemente incomprensible, nos enfocaremos en el código de las líneas 4 a 6:
def (n)
return def ()
print(n)
end
end (i)
Aquí realmente se define una función anónima y se llama inmediatamente.
La función de esta función anónima temporal es vincular el valor de la
variable de bucle i
a su parámetro n
, y la variable n
también es lo que necesitamos para cerrar las variables libres del
paquete, de modo que las las variables vinculadas al cierre construido
durante cada ciclo son diferentes. Ahora obtendremos la salida deseada:
res = func()
res[0]() # 0
res[1]() # 1
res[2]() # 2
Hay algunas formas de resolver el problema de las variables de bucle como variables libres. Una forma un poco más simple es definir una variable temporal en el cuerpo del bucle:
def func()
l = [] i = 0
while i <= 2
temp = i
l.push(def () print(temp) end)
i = i + 1
end
return l
end
Aquí temp
es una variable temporal. El alcance de esta variable está
en el cuerpo del ciclo, por lo que se redefinirá cada vez que se realice
un ciclo. También podemos usar la instrucción for
para resolver el
problema:
def func()
l = []
for i: 0 .. 2
l.push(def () print(i) end)
end
return l
end
Esta puede ser la forma más sencilla defor
. La variable de
iteración de la instrucción se creará en cada ciclo. El principio es
similar al método anterior.
Expresión lambda¶
La Expresión lambda es una función anónima especial. La expresión lambda se compone de una lista de parámetros y un cuerpo de función, pero la forma es diferente de la función general:
´/´ args ´->´ expr ´end’
args es la lista de parámetros, la cantidad de parámetros puede ser cero o más, y los parámetros múltiples están separados por comas o espacios (no se pueden mezclar al mismo tiempo); expr es la expresión de retorno, la expresión lambda devolverá el valor de la expresión. Las expresiones lambda son adecuadas para implementar funciones muy simples. Por ejemplo, la expresión lambda para juzgar el tamaño de dos números es:
/ a b -> a < b
Esto es más fácil que escribir una función con la misma funcionalidad. En algunos algoritmos generales de clasificación, este tipo de función de comparación de tamaño puede necesitar un uso extensivo. El uso de expresiones lambda puede simplificar el código y mejorar la legibilidad.
Al igual que las funciones generales, las expresiones lambda pueden formar cierres. Las expresiones lambda se llaman de la misma manera que las funciones ordinarias. Si usa el método de llamada inmediata similar a las funciones anónimas:
lambda = / a b -> a < b
result = lambda(1, 2) # llamada normal
result = (/ a b -> a < b)(1, 2) # llamada directa
Dado que el operador de llamada de función tiene una prioridad más alta, se debe agregar un par de paréntesis a la expresión lambda cuando se realiza una llamada directa, para que se llame como un todo.