Funciones
Las funciones son bloques de código que pueden ser reutilizados en diferentes partes del programa, pueden aceptar parámetros de entrada y retornar un valor de salida. Esto ayuda a organizar el código y a hacerlo más legible y mantenible. Además, al usar funciones, es posible evitar la repetición de código y facilitar la depuración.
Al igual que las variables y los objetos, las funciones también son valores de primera clase en JARU, lo que significa que pueden ser asignados a variables, pasados como parámetros a otras funciones y retornados como valor de otras funciones.
Una función admite hasta 255 parámetros, aunque antes de llegar ahí manda la pila (ver Límites de la pila). Si dentro de la función se asigna un valor nuevo a un parámetro, la variable que se pasó no cambia. En cambio, las listas, los mapas, los arrays, las instancias y los structs no se copian: si la función cambia su contenido (por ejemplo, con append), el cambio también se ve fuera de la función.
Declaración de función
Para declarar una función en JARU, se utiliza la palabra clave func seguida del nombre que quieras darle a la función y de los parámetros entre paréntesis, y se termina con la palabra clave end.
func miFuncion(parametro1, parametro2)
// cuerpo de la función
end
Los parámetros de una función son variables que reciben los valores que se pasan a la función al llamarla. Una función puede no tener ninguno o tener varios. Asignar un valor nuevo a un parámetro solo cambia el parámetro, no la variable original; pero si el parámetro es una lista, un mapa, un array, una instancia o un struct, la función trabaja sobre el mismo objeto, y los cambios en su contenido se ven fuera:
func cambia(numero, lista)
numero = 99 // no cambia la variable de fuera
lista.append(4) // cambia la lista de fuera
end
var n = 1
var l = [1, 2, 3]
cambia(n, l)
println(n) // 1
println(l) // [1, 2, 3, 4]
Para retornar un valor de una función se utiliza la palabra clave return seguida del valor a retornar. Si una función no retorna ningún valor, no es necesario utilizar la palabra clave return.
Realmente en JARU todas las funciones retornan un valor. Si en la función no se utiliza return, devolverá el valor nil.
Ejemplos de declaración de funciones
// Función que no recibe parámetros y no retorna ningún valor
func imprimir_mensaje()
print("Hola, mundo")
end
// Función que recibe parámetros y retorna un valor
func sumar(num1, num2)
return num1 + num2
end
// Función que recibe varios parámetros y no retorna ningún valor
func imprimir_datos(nombre, edad, ciudad)
print("Nombre: ", nombre, " Edad: ", edad, " Ciudad: ", ciudad)
end
Sin valores por defecto
Los parámetros no admiten valores por defecto. Esto no compila:
func dispara(x, y, velocidad = 5)
println(x, y, velocidad)
end
Si quieres un parámetro opcional, pásale nil y decide dentro qué valor usar:
func dispara(x, y, velocidad)
if (velocidad == nil) then
velocidad = 5
end
println("disparo en ", x, ", ", y, " a ", velocidad)
end
dispara(10, 20, nil) // disparo en 10, 20 a 5
dispara(10, 20, 8) // disparo en 10, 20 a 8
Tampoco puedes tener dos funciones con el mismo nombre y distintos parámetros: la segunda declaración sustituye a la primera. Solo los constructores de las clases admiten varias versiones (ver Sobrecarga de constructores).
Llamar a una función
Llamar a una función es el proceso de ejecutar el código contenido dentro de esa función. En JARU, se puede llamar a una función utilizando su nombre seguido de paréntesis y, opcionalmente, pasando argumentos dentro de los paréntesis.
imprimir_datos("Juan", 25, "Madrid")
En este caso, la función imprimir_datos recibiría los argumentos "Juan", 25 y "Madrid" y los utilizaría para imprimir un mensaje en pantalla.
También es posible almacenar el valor retornado por una función en una variable:
var resultado = sumar(3, 4)
En este caso, la variable resultado almacenaría el valor 7.
Cada llamada tiene que pasar tantos argumentos como parámetros tiene la función. Si pasas más o menos, el error salta al ejecutar la llamada: ERR_INVALID_ARG_NUMBER (Function 'sumar' expects 2 arguments, but got 1.).
Una función puede llamar a otras funciones, y también declarar funciones dentro de su cuerpo: ver Declaración anidada de funciones.
Antes de declararla
Una función existe desde que se ejecuta su func, igual que una variable desde su var. Si la llamas más arriba, el programa compila, pero se para al llegar a la llamada con el error ERR_NAME_NOT_FOUND (Undefined variable 'saluda'.):
saluda() // ERR_NAME_NOT_FOUND: todavía no existe
func saluda()
println("Hola")
end
Dentro de una función, en cambio, sí puedes llamar a otra que esté declarada más abajo: lo que cuenta es que exista cuando la llamada se ejecuta. Así, dos funciones se pueden llamar entre sí:
func esPar(n)
if (n == 0) then
return true
end
return esImpar(n - 1)
end
func esImpar(n)
if (n == 0) then
return false
end
return esPar(n - 1)
end
println(esPar(10)) // true
println(esImpar(7)) // true
Ámbito
Una función ve tres clases de variables:
- Sus parámetros y sus variables locales, las que declara con
vardentro. Solo existen mientras la función se ejecuta. - Las variables globales, las declaradas fuera de cualquier función y de cualquier bloque. Una función puede leerlas y cambiarlas sin declarar nada. Basta con que existan cuando la función se ejecuta, aunque estén declaradas más abajo.
- Las variables de las funciones que la contienen, si está declarada dentro de otra (ver Captura de variables).
Una variable local con el mismo nombre que una global la tapa: dentro de la función, ese nombre es la local, y la global no cambia.
var vidas = 3
func pierdeVida()
vidas = vidas - 1 // cambia la global
puntos = puntos + 10 // también, aunque se declare más abajo
end
func prueba()
var vidas = 99 // local: tapa a la global
println("dentro: ", vidas)
end
var puntos = 0
pierdeVida()
println(vidas, " ", puntos) // 2 10
prueba() // dentro: 99
println(vidas) // 2
Una variable declarada dentro de un bloque (if, for, while...) solo existe hasta su end, también dentro de una función (ver Variables y constantes).
Usar un nombre que no existe no da error al compilar, sino al ejecutar: ERR_NAME_NOT_FOUND (Undefined variable 'doble'.). Pasa al asignar una variable que no has declarado con var, y al usar una variable de un bloque fuera de él:
func calcula(n)
if (n > 0) then
var doble = n * 2
end
return doble // ERR_NAME_NOT_FOUND: doble ya no existe aquí
end
Declaración anidada de funciones
En JARU, se pueden declarar funciones dentro de otras funciones:
func funcion_padre()
func funcion_hija()
println("Soy la función hija")
end
println("Soy la función padre")
funcion_hija()
end
funcion_padre()
En este ejemplo, la función funcion_padre es la función principal que contiene a funcion_hija dentro de ella. La función funcion_hija solo se puede llamar dentro de funcion_padre, ya que está declarada dentro de esa función.
También es posible pasar parámetros a la función hija y devolver valores de la misma:
func funcion_padre()
func funcion_hija(num1, num2)
return num1 + num2
end
var resultado = funcion_hija(2, 3)
print("El resultado es: ", resultado)
end
funcion_padre()
En consola se mostrará: El resultado es: 5
Funciones anónimas (lambdas fn)
Una lambda es una función sin nombre que se usa como un valor: se puede guardar en una variable, meter en una lista o mapa, devolver desde una función, o pasar directamente como callback. Se escribe con la palabra clave fn, la lista de parámetros, dos puntos y el cuerpo terminado en end:
fn(parametros): cuerpo end
// En una variable
var doble = fn(x): return x * 2 end
println(doble(21)) // 42
// Directamente como callback: el caso de uso estrella
use Timer
Timer.every(1000, fn(id, p, u):
println("tic")
end)
// En listas y mapas (tablas de despacho)
var comandos = {"saluda": fn(quien): println("Hola, ", quien) end}
var saluda = comandos["saluda"]
saluda("Jon")
println, len, sqrt y las demás funciones de la Librería estándar no se pueden guardar en una variable ni pasar como argumento: var f = len no compila. Las de los módulos tampoco: var s = Math.sin se para al ejecutar. Envuélvelas en una lambda:
use Math
var seno = fn(x): return Math.sin(x) end
println(seno(0)) // 0
Captura de variables (closures)
Una lambda puede usar las variables del ámbito donde se creó. La captura es por referencia: la lambda ve los cambios posteriores de la variable, y la variable sigue viva mientras la lambda exista — incluso si la función donde nació ya terminó:
func hacerContador()
var n = 0
return fn():
n = n + 1
return n
end
end
var c1 = hacerContador()
var c2 = hacerContador()
println(c1(), " ", c1(), " ", c1()) // 1 2 3
println(c2()) // 1 (cada contador tiene su propia n)
Dentro de un método de clase, una lambda también puede capturar this:
class Boton
def init()
this.pulsado = 0
end
def manejador()
return fn(): this.pulsado++ end
end
end
Recursión con lambdas
Una lambda no tiene nombre, así que, para la recursión, se asigna primero a una variable y se llama a través de ella:
var fact = nil
fact = fn(n):
if (n <= 1) then
return 1
end
return n * fact(n - 1)
end
println(fact(5)) // 120
Si una función devuelve otra función, puedes llamar al resultado directamente: crea()(x). Lo que todavía no funciona es indexar lo que devuelve una llamada, f(x)[0]: compila, pero al ejecutar da el error This object type is not indexable. Guarda antes el resultado en una variable: var r = f(x) y luego r[0].
Recursión
En JARU se permite la recursión, esto es, que una función se llame a sí misma para resolver un problema. La recursividad se utiliza a menudo para resolver problemas que pueden ser divididos en subproblemas más pequeños del mismo tipo.
Para crear una función recursiva en JARU, se debe definir la función de la misma manera que se define cualquier otra función, pero dentro de la función, se debe incluir una llamada a la misma función.
func factorial(n)
if (n == 0) then
return 1
else
return n * factorial(n - 1)
end
end
En este ejemplo, la función factorial se llama a sí misma con un parámetro uno menor hasta que se alcanza el caso base (n == 0).
Una función recursiva necesita un caso base al que llegue siempre. Sin él, se llama a sí misma hasta llenar la pila de la VM, y la VM se cierra. Y aunque lo tenga, cada llamada ocupa sitio en la pila: la recursión no puede bajar más de unas decenas o pocos cientos de niveles (ver Límites de la pila).
Ejemplos comunes de recursión
- Algoritmos de búsqueda y ordenamiento: Algoritmos como el ordenamiento quicksort y el algoritmo de búsqueda binaria utilizan recursión para dividir un problema en problemas más pequeños y resolverlos de forma independiente.
- Problemas de vuelta atrás (backtracking): Problemas como el del ratón en el laberinto y la solución de sudokus pueden resolverse utilizando recursión para probar diferentes opciones y retroceder cuando se llega a un camino sin solución.
- Problemas matemáticos: Problemas como la serie de Fibonacci y el cálculo del factorial pueden resolverse de forma eficiente utilizando recursión.
- Problemas de grafos: Algoritmos como la búsqueda en profundidad y la búsqueda en anchura para recorrer un grafo utilizan recursión para explorar los diferentes nodos.
La recursión es una herramienta poderosa para resolver problemas que se pueden dividir en subproblemas independientes, y JARU te permite usarla.
Límites de la pila
Cada llamada en curso guarda en la pila de la VM la función, sus argumentos, sus variables locales y las cuentas que tiene a medias. Esa pila tiene sitio para 256 valores para todo el programa, y es del mismo tamaño en Windows y en las placas.
Por eso la recursión tiene un límite, que depende de lo que guarda cada llamada. Medido en Windows:
| Cada llamada | Niveles |
|---|---|
Un parámetro y nada más: baja(n + 1) | 127 |
Un parámetro y una suma a medias: return n + suma(n - 1) | 85 |
| Tres parámetros y dos variables locales | 42 |
Con muchos argumentos pasa lo mismo: el compilador admite hasta 255 parámetros, pero una llamada con 254 argumentos ya no cabe en la pila.
Si una recursión o una cadena de llamadas llena la pila, la VM se cierra sin ningún mensaje (en Windows; en las placas no se ha probado), y try no lo captura: la constante ERR_RECURSION_DEPTH_EXCEEDED existe, pero la VM no la lanza. Si una función puede llamarse a sí misma más de unas decenas de veces, escríbela con un bucle.
Todos los problemas conocidos de JARU, juntos, están en Problemas conocidos.
// Recursiva: con n grande se queda sin pila
func sumaRecursiva(n)
if (n == 0) then
return 0
end
return n + sumaRecursiva(n - 1)
end
// Con un bucle: vale para cualquier n
func sumaBucle(n)
var total = 0
for (var i = 1; i <= n; i++)
total = total + i
end
return total
end
println(sumaRecursiva(50)) // 1275
println(sumaBucle(1000)) // 500500