Saltar al contenido

Funciones como valores y closures

En la lección anterior escribiste funciones y las llamaste por su nombre. En Go, una función también es un valor, como un número o un texto. Al terminar esta lección vas a poder guardar una función en una variable, pasarla a otra función, devolverla y escribir funciones sin nombre que recuerdan variables: los closures. También vas a saber leer los errores que aparecen cuando una función no es del tipo que se espera o cuando falta llamarla.

Una función es un valor

La Papelería Central tiene dos ofertas: la mitad del precio, o 300 menos. Las dos son funciones, y una variable puede guardar cualquiera de las dos:

main.go
package main

import "fmt"

func mitad(precio int) int {
	return precio / 2
}

func menos300(precio int) int {
	return precio - 300
}

func main() {
	descuento := mitad
	fmt.Println(descuento(1500))

	descuento = menos300
	fmt.Println(descuento(1500))

	fmt.Printf("%T\n", descuento)
}

Salida:

750
1200
func(int) int

descuento := mitad no llama a mitad: no hay paréntesis. Sin paréntesis, mitad es la función misma, y := la guarda en la variable descuento. Con paréntesis, mitad(1500) es una llamada, y vale lo que devuelve.

descuento(1500) llama a la función que guarda descuento. Primero guarda mitad, así que da 750. Después, descuento = menos300 le da otra función, y la misma llamada da 1200.

%T muestra el tipo de descuento: func(int) int, una función que recibe un int y devuelve un int. Como el de toda variable, ese tipo queda fijo: descuento solo puede guardar funciones que reciben un int y devuelven un int.

Si imprimes la variable sin llamarla, con fmt.Println(descuento), no ves 750. Ves un número como 0xe5ad380: el lugar de la función en la memoria de la computadora. Si ves algo así, faltan los paréntesis de la llamada.

Solo se llama a una función

Los paréntesis después de un nombre lo llaman. Si el nombre no guarda una función, el programa no compila. Aquí, descuento guarda un monto, no una función:

main.go
package main

import "fmt"

func main() {
	descuento := 500
	fmt.Println(descuento(1500))
}

Salida:

./main.go:7:14: invalid operation: cannot call descuento (variable of type int): int is not a function

Lee el mensaje por partes:

  • ./main.go:7:14 apunta a descuento(1500), en la línea 7.
  • invalid operation: cannot call descuento significa “operación no válida: no se puede llamar a descuento”.
  • (variable of type int) dice qué es descuento: una variable de tipo int.
  • int is not a function: un int no es una función.

Si descuento tiene que ser un monto, réstalo: 1500 - descuento. Si tiene que ser una función, guarda una en la variable, como en descuento := mitad.

Tipos función

Una función puede recibir otra función como parámetro. aplicar recibe un precio y la oferta que hay que aplicarle:

main.go
package main

import "fmt"

// aplicar devuelve el precio después de pasarlo por descuento.
func aplicar(precio int, descuento func(int) int) int {
	return descuento(precio)
}

func mitad(precio int) int {
	return precio / 2
}

func menos300(precio int) int {
	return precio - 300
}

func main() {
	fmt.Println("Cuaderno:", aplicar(1500, mitad))
	fmt.Println("Mochila:", aplicar(4200, menos300))
}

Salida:

Cuaderno: 750
Mochila: 3900

El parámetro descuento es de tipo func(int) int. Es un tipo función: la palabra func, los tipos de los parámetros entre paréntesis y, después, el tipo del resultado. Algunos ejemplos:

  • func(): no recibe nada y no devuelve nada.
  • func(int) int: recibe un int y devuelve un int.
  • func(int, int) int: recibe dos int y devuelve un int.
  • func(string) bool: recibe un string y devuelve un bool.

aplicar no sabe qué oferta va a recibir: llama a la que le pasan. En la primera llamada, descuento es mitad, y aplicar devuelve 750. En la segunda es menos300, y devuelve 3900. Las funciones se pasan por su nombre, sin paréntesis: aplicar(1500, mitad).

Un tipo función también puede llevar los nombres de los parámetros, como func(precio int) int. Los nombres no cuentan: es el mismo tipo que func(int) int. Para que una función sirva donde va un tipo función, tiene que recibir los mismos tipos, en el mismo orden, y devolver el mismo tipo.

Una función de otro tipo

Esta mitad devuelve un float64, con decimales. Recibe un int, como pide aplicar, pero no devuelve un int:

main.go
package main

import "fmt"

func aplicar(precio int, descuento func(int) int) int {
	return descuento(precio)
}

func mitad(precio int) float64 {
	return float64(precio) / 2
}

func main() {
	fmt.Println(aplicar(1500, mitad))
}

Salida:

./main.go:14:28: cannot use mitad (value of type func(precio int) float64) as func(int) int value in argument to aplicar

Es el mensaje cannot use que ya conoces (“no se puede usar”), ahora con tipos función:

  • mitad (value of type func(precio int) float64) dice qué es mitad: una función que recibe un int y devuelve un float64. Go escribe el nombre del parámetro, precio, pero no cuenta.
  • as func(int) int value dice qué se esperaba: una función que devuelve un int.
  • in argument to aplicar dice dónde: en un argumento de la llamada a aplicar.

Para corregirlo, haz que mitad devuelva un int: func mitad(precio int) int, con return precio / 2.

Otro error parecido es pasar lo que devuelve la función en vez de la función: con aplicar(1500, mitad(1500)), Go responde cannot use mitad(1500) (value of type int). mitad(1500) ya es una llamada, y vale 750, un int. Pasa la función sin paréntesis, y aplicar la llama.

Si vienes de JavaScript, Python o Java…

En JavaScript y en Python también pasas funciones como valores, pero sin escribir su tipo: cualquier función entra en el parámetro, y el problema aparece al ejecutar. En Java, un parámetro así es una interfaz, como Function<Integer, Integer>, y le pasas una referencia como Papeleria::mitad. En Go escribes el tipo función en la firma, func(int) int, y el compilador revisa cada llamada.

Funciones anónimas

Una función también se puede escribir sin nombre, justo donde se usa:

main.go
package main

import "fmt"

func aplicar(precio int, descuento func(int) int) int {
	return descuento(precio)
}

func main() {
	menos10PorCiento := func(precio int) int {
		return precio - precio/10
	}
	fmt.Println("Regla:", aplicar(800, menos10PorCiento))

	fmt.Println("Lápiz:", aplicar(500, func(precio int) int {
		return precio - 100
	}))
}

Salida:

Regla: 720
Lápiz: 400

func(precio int) int { … }, sin nombre después de func, es una función anónima. Tiene parámetros, resultado y cuerpo, como cualquier función, pero es un valor: va donde va un valor. Como en toda función, la { va al final de su primera línea.

  • La primera se guarda en la variable menos10PorCiento con :=, y después se pasa a aplicar. 800 menos su décima parte, 80, da 720.
  • La segunda se escribe directamente como argumento de aplicar. Su } cierra la función anónima, y los dos ) que siguen cierran aplicar( y fmt.Println(. Da 500 - 100, que es 400.

Una función anónima se puede escribir dentro de main o de otra función. Una función con nombre, no: va siempre afuera, como viste en la lección Funciones.

Como toda variable, una que guarda una función tiene que usarse. Si nunca la llamas ni la pasas, Go responde declared and not used: menos10PorCiento.

Una función anónima suelta

Escrita sola en su línea, una función anónima no hace nada, y el programa no compila:

main.go
package main

import "fmt"

func main() {
	func() {
		fmt.Println("Bienvenido a la Papelería Central")
	}
	fmt.Println("¿Qué vas a llevar?")
}

Salida:

./main.go:6:2: func() {…} (value of type func()) is not used

func() {…} es la función anónima; Go abrevia su cuerpo con {…}. (value of type func()) dice que es un valor de tipo func(), y is not used, que no se usa: nadie la llama ni la guarda.

Para usarla después, guárdala en una variable, como bienvenida := func() { … }, y llámala con bienvenida(). Si querías ejecutarla en ese mismo lugar, agrega () después de su }: func() { … }() la llama en el momento. Sola, esa forma se usa poco; es común junto con defer, el tema de la lección siguiente.

Consejo

Si la función es corta y la usas una sola vez, escríbela anónima, en el lugar donde la usas. Si es larga o la usas en varios lugares, dale un nombre: se lee mejor aplicar(1500, mitad) que diez líneas dentro de una llamada.

Closures

Una función anónima puede usar las variables del lugar donde se escribe:

main.go
package main

import "fmt"

func main() {
	vendidos := 0
	vender := func(cantidad int) {
		vendidos += cantidad
	}

	vender(3)
	vender(2)
	fmt.Println("Cuadernos vendidos:", vendidos)
}

Salida:

Cuadernos vendidos: 5

vendidos está declarada en main, fuera de la función anónima, y la función la usa igual. No recibe una copia: usa la misma variable. Por eso cada llamada a vender cambia el vendidos de main, y al final vale 3 + 2, que es 5.

Una función que usa variables de afuera es un closure (en español, “clausura”, pero en Go se dice closure). Se dice que la función captura esas variables: vender captura vendidos.

Funciones que devuelven funciones

Una función también puede devolver otra. descuentoFijo arma una oferta de un monto fijo:

main.go
package main

import "fmt"

// descuentoFijo devuelve una función que le resta monto a un precio.
func descuentoFijo(monto int) func(int) int {
	return func(precio int) int {
		return precio - monto
	}
}

func main() {
	menos300 := descuentoFijo(300)
	menos500 := descuentoFijo(500)
	fmt.Println("Cuaderno:", menos300(1500))
	fmt.Println("Mochila:", menos500(4200))
	fmt.Println("Regla:", descuentoFijo(100)(800))
}

Salida:

Cuaderno: 1200
Mochila: 3700
Regla: 700

El tipo del resultado de descuentoFijo es func(int) int, y su return devuelve una función anónima de ese tipo. Esa función es un closure: usa monto, el parámetro de descuentoFijo.

Cada llamada a descuentoFijo crea una función nueva, con su propio monto. menos300 recuerda 300, y menos500 recuerda 500. descuentoFijo ya terminó cuando se llaman, pero su monto sigue existiendo, porque el closure lo usa.

descuentoFijo(100)(800) hace las dos cosas en una línea. Los primeros paréntesis llaman a descuentoFijo y dan una función; los segundos llaman a esa función con 800. Los mensajes de los ejercicios usan esta forma: descuentoDe(20)(1500).

Un contador con closure

Un closure también puede cambiar la variable que captura, y esa variable sigue viva entre una llamada y la siguiente. Así funciona un contador de turnos para cada caja:

main.go
package main

import "fmt"

// contadorDeTurnos devuelve una función que da el turno siguiente.
func contadorDeTurnos() func() int {
	turno := 0
	return func() int {
		turno++
		return turno
	}
}

func main() {
	cajaUno := contadorDeTurnos()
	cajaDos := contadorDeTurnos()
	fmt.Println("Caja 1, turno", cajaUno())
	fmt.Println("Caja 1, turno", cajaUno())
	fmt.Println("Caja 2, turno", cajaDos())
	fmt.Println("Caja 1, turno", cajaUno())
}

Salida:

Caja 1, turno 1
Caja 1, turno 2
Caja 2, turno 1
Caja 1, turno 3

Cada llamada a contadorDeTurnos declara un turno nuevo, en 0, y devuelve un closure que lo captura. Cada vez que llamas a ese closure, suma 1 a su turno y lo devuelve.

cajaUno y cajaDos vienen de llamadas distintas, así que cada una tiene su propio turno. Por eso cajaDos() da 1 aunque cajaUno ya iba en 2, y la siguiente llamada a cajaUno() sigue en 3.

Ninguna otra parte del programa puede cambiar esos turno: solo el closure que los capturó. Así, un closure guarda un dato que cambia entre llamadas, sin que nadie más pueda tocarlo.

Si vienes de JavaScript, Python o Java…

En JavaScript, los closures funcionan igual. En Python, para cambiar una variable capturada hace falta declararla con nonlocal turno; en Go, el closure la cambia directamente. En Java, una lambda solo puede usar variables locales que no cambian (effectively final): este contador no compila con una lambda.

En Go no hay una forma corta como (precio) => precio / 2 de JavaScript o lambda precio: precio // 2 de Python: una función anónima se escribe siempre con func, los tipos y las llaves.

La variable del for, una por vuelta

Un closure creado dentro de un for puede usar la variable del for. La caja arma un anuncio por turno y guarda el del primero y el del último para llamarlos al final:

main.go
package main

import "fmt"

func main() {
	var primero func()
	var ultimo func()
	for turno := 1; turno <= 3; turno++ {
		anunciar := func() {
			fmt.Println("Atendiendo el turno", turno)
		}
		if turno == 1 {
			primero = anunciar
		}
		ultimo = anunciar
	}
	primero()
	ultimo()
}

Salida:

Atendiendo el turno 1
Atendiendo el turno 3

var primero func() declara una variable de tipo func() sin darle valor. Su valor cero es nil, que significa “ninguna función”. Llamarla mientras vale nil detiene el programa con un error; aquí las dos reciben su función dentro del for, antes de llamarlas. Para saber si una variable función ya guarda una, se compara con nil (if primero != nil), y solo con nil: descuento == mitad no compila.

En cada vuelta, anunciar es un closure nuevo, que captura turno. El de la primera vuelta se guarda en primero. ultimo recibe el de cada vuelta, así que al final tiene el de la tercera. primero() imprime 1 y ultimo() imprime 3: cada closure recuerda el turno de su vuelta.

Funciona así porque, desde Go 1.22, cada vuelta de un for tiene su propia variable. Go crea un turno nuevo en cada vuelta, con el valor que tenía el de la vuelta anterior, y después le aplica el paso, turno++. Lo mismo pasa con la variable de un for con range.

Antes de Go 1.22, el for tenía una sola variable turno para todas las vueltas, y cada vuelta cambiaba su valor. Los tres closures capturaban esa misma variable. Al terminar el for, turno valía 4, el valor con el que turno <= 3 dio false. El mismo programa imprimía Atendiendo el turno 4 dos veces.

Era un error muy común. La corrección de entonces era copiar la variable al principio del cuerpo del for: turno := turno declara otra variable, de esa vuelta, con el mismo nombre.

Un proyecto que declara, en su archivo go.mod, una versión de Go anterior a la 1.22 conserva el comportamiento viejo; en este curso, siempre tienes el nuevo.

Consejo

En código escrito antes de Go 1.22 vas a ver turno := turno o i := i dentro de un for. Desde Go 1.22 esa copia sobra, y puedes quitarla (Fixing For Loops in Go 1.22).

Si vienes de JavaScript, Python o Java…

En JavaScript pasaba lo mismo con var i en un for: una sola variable para todas las vueltas. Con let i, cada vuelta tiene la suya, como en Go desde la 1.22. En Python, las funciones creadas dentro de un for comparten la variable: si las llamas después del for, todas ven el valor de la última vuelta.

En resumen

  • Una función es un valor: se guarda en una variable (descuento := mitad), se pasa como argumento y se devuelve. Sin paréntesis es la función misma; con paréntesis, una llamada.
  • Un tipo función, como func(int) int, dice qué recibe y qué devuelve una función. Una función solo sirve donde va un tipo función si recibe y devuelve los mismos tipos (cannot use … as func(int) int value).
  • func(precio int) int { … } sin nombre es una función anónima: un valor que se escribe donde se usa. Sola en su línea, Go responde is not used.
  • Un closure es una función que usa variables de afuera. Usa la misma variable, no una copia, y la variable sigue viva mientras el closure exista, como el turno de un contador.
  • Desde Go 1.22, cada vuelta de un for tiene su propia variable, y cada closure creado en una vuelta recuerda el valor de esa vuelta.

Ejercicios

Escribe la función aplicar(precio int, descuento func(int) int) int, que devuelve lo que vale precio después de pasarlo por la función descuento.

Una prueba oculta llama a tu función; main no se ejecuta.

  main.go

Para salir del editor con el teclado, pulsa Escape y luego Tab.

package main

import "fmt"

// Escribe aquí la función aplicar, fuera de main.

func main() {
	fmt.Println("Ofertas de la Papelería Central")
}
 

¿Ver la solución?

La solución reemplazará tu código en el editor. Antes de verla, prueba con una pista: se aprende más cuando lo resuelves tú.