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.