Swift de Cero a Experto #5: Funciones — ciudadanos de primera clase
Parámetros, labels, inout, function types y funciones como valores. La puerta de entrada a los closures y la programación funcional.
En el artículo anterior dominamos el control de flujo — switch con pattern matching, guard como filosofía, y cómo el compilador convierte tus decisiones en jump tables. Hoy damos un paso que cambia todo: las funciones.
Y no, no me refiero a “bloques de código que reciben parámetros y retornan un valor”. Eso lo sabes desde tu primer día programando. Lo que hace especial a las funciones en Swift es que son ciudadanos de primera clase — valores que puedes almacenar en variables, pasar como parámetros y retornar desde otras funciones. Exactamente como un Int o un String.
Entender esto es la puerta de entrada a los closures (artículo #6), la programación funcional (artículo #20), y la forma en que Swift piensa.
En Swift, una función no es solo algo que ejecutas — es un valor que puedes manipular. Y esa idea lo cambia todo.

Definiendo y llamando funciones
La anatomía básica de una función en Swift, según la documentación oficial:
func greet(person: String) -> String { let message = "Hello, " + person + "!" return message}
print(greet(person: "Anna")) // "Hello, Anna!"print(greet(person: "Brian")) // "Hello, Brian!"Nada sorprendente aquí. Pero vamos a ir desarmando cada pieza para entender las decisiones de diseño.
Implicit return
Si el cuerpo de tu función es una sola expresión, puedes omitir el return:
func greet(person: String) -> String { "Hello, " + person + "!"}El compilador entiende que la única expresión es el valor de retorno. Es lo mismo que escribir return — sin costo extra, solo menos ruido visual.
Parámetros y valores de retorno
Sin parámetros
func sayHello() -> String { return "Hello, world"}Sin valor de retorno
func printGreeting(person: String) { print("Hello, \(person)!")}Técnicamente, una función sin retorno sí retorna algo: Void — que es simplemente una tupla vacía ().
Múltiples valores de retorno con tuplas
func minMax(array: [Int]) -> (min: Int, max: Int)? { if array.isEmpty { return nil }
var currentMin = array[0] var currentMax = array[0]
for value in array[1..<array.count] { if value < currentMin { currentMin = value } else if value > currentMax { currentMax = value } }
return (currentMin, currentMax)}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) { print("min is \(bounds.min) and max is \(bounds.max)") // "min is -6 and max is 109"}Nota que el tipo de retorno es (min: Int, max: Int)? — un optional tuple. Si el array está vacío, retorna nil. Las etiquetas min y max permiten acceder a los valores por nombre en lugar de .0 y .1.
Argument Labels y Parameter Names: por qué Swift tiene dos nombres
Esta es una de las decisiones de diseño más distintivas de Swift. Cada parámetro tiene dos nombres: un argument label (para quien llama) y un parameter name (para el cuerpo de la función).
func greet(person: String, from hometown: String) -> String { return "Hello \(person)! Glad you could visit from \(hometown)."}
// Al llamar, se lee como una oración:greet(person: "Bill", from: "Cupertino")persones tanto label como parameter name (el default)fromes el argument label,hometownes el parameter name

¿Por qué? Porque Swift hereda la filosofía de Objective-C de que las llamadas a funciones deben leerse como oraciones en inglés. greet(person: "Bill", from: "Cupertino") se lee naturalmente.
Omitir el label con _
func add(_ a: Int, _ b: Int) -> Int { return a + b}
add(3, 5) // Sin labels — para operaciones matemáticas tiene sentidoValores por defecto
func connect(to host: String, port: Int = 443, secure: Bool = true) { print("Connecting to \(host):\(port) (secure: \(secure))")}
connect(to: "api.example.com") // port=443, secure=trueconnect(to: "api.example.com", port: 8080) // secure=trueconnect(to: "api.example.com", secure: false) // port=443Los parámetros con defaults van al final. Los argumentos por defecto los sintetiza el compilador y los inyecta en el call site — no se generan overloads extra. La expresión por defecto se evalúa en cada llamada que la omite, así que es prácticamente gratis para literales pero no necesariamente de costo cero (por ejemplo, un default de Date() se ejecuta cada vez).
Variadic Parameters
func average(_ numbers: Double...) -> Double { var total: Double = 0 for number in numbers { total += number } return total / Double(numbers.count)}
average(1, 2, 3, 4, 5) // 3.0average(3, 8.25, 18.75) // 10.0Dentro del cuerpo, numbers es un [Double] — un array normal. El ... es azúcar sintáctica para quien llama la función.
In-Out Parameters: modificando valores externos
Por defecto, los parámetros de una función son constantes — no puedes modificarlos. Si necesitas que la función modifique un valor externo, usas inout:
func swapValues(_ a: inout Int, _ b: inout Int) { let temp = a a = b b = temp}
var x = 3var y = 107swapValues(&x, &y)print("x is \(x), y is \(y)")// "x is 107, y is 3"El & al pasar el argumento indica que la función puede modificar esa variable. Es explícito — no hay mutaciones ocultas.

Function Types: las funciones como valores
Aquí es donde las cosas se ponen interesantes. Cada función tiene un tipo definido por sus parámetros y su retorno:
func addTwoInts(_ a: Int, _ b: Int) -> Int { a + b }func multiplyTwoInts(_ a: Int, _ b: Int) -> Int { a * b }
// Ambas funciones tienen tipo: (Int, Int) -> IntY puedes usar ese tipo como cualquier otro tipo en Swift:
// Almacenar una función en una variablevar mathFunction: (Int, Int) -> Int = addTwoIntsprint(mathFunction(2, 3)) // 5
// Reasignar a otra función del mismo tipomathFunction = multiplyTwoIntsprint(mathFunction(2, 3)) // 6
Asigna diferentes funciones a la misma variable y el resultado cambia — pero el tipo siempre es (Int, Int) -> Int:

Cuando una función cabe en una variable, deja de ser “algo que ejecutas” y se convierte en “algo que manipulas”. Eso es lo que significa ser first-class citizen.
Funciones como parámetros
Puedes pasar funciones como argumentos a otras funciones:
func printMathResult(_ operation: (Int, Int) -> Int, _ a: Int, _ b: Int) { print("Result: \(operation(a, b))")}
printMathResult(addTwoInts, 3, 5) // "Result: 8"printMathResult(multiplyTwoInts, 3, 5) // "Result: 15"printMathResult no sabe ni le importa qué hace operation — solo sabe que acepta dos Int y retorna un Int. Esto es polimorfismo (el mismo código funcionando con muchos comportamientos distintos) a través de tipos de función — uno de los pilares de la programación funcional.
Funciones como valor de retorno
func stepForward(_ input: Int) -> Int { input + 1 }func stepBackward(_ input: Int) -> Int { input - 1 }
func chooseStepFunction(backward: Bool) -> (Int) -> Int { return backward ? stepBackward : stepForward}
var currentValue = 3let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)// moveNearerToZero ahora ES stepBackward
while currentValue != 0 { print("\(currentValue)... ") currentValue = moveNearerToZero(currentValue)}print("zero!")// 3... 2... 1... zero!chooseStepFunction retorna una función — no un valor, sino comportamiento. El tipo de retorno (Int) -> Int es una función que toma un Int y retorna un Int.
Nested Functions: closures disfrazados
Puedes definir funciones dentro de funciones:
func chooseStepFunction(backward: Bool) -> (Int) -> Int { func stepForward(input: Int) -> Int { input + 1 } func stepBackward(input: Int) -> Int { input - 1 } return backward ? stepBackward : stepForward}Las nested functions están ocultas del mundo exterior — solo su función contenedora puede verlas. Pero cuando una nested function es retornada fuera de su scope, se convierte en algo más poderoso: un closure.
¿Por qué? Porque puede capturar (mantener una referencia a) variables del scope que la rodea para que sigan vivas después de que la función externa retorne. Y eso tiene implicaciones directas en memoria — que exploraremos a fondo en el próximo artículo.
La memoria detrás de las funciones
¿Dónde vive una función?
El código ejecutable de una función vive en el segmento __TEXT del binario Mach-O — como vimos en Dominando Instruments (Parte 2). Es de solo lectura y se comparte entre procesos. Cuando llamas a una función, el procesador salta a esa dirección de memoria.
¿Qué pasa en el stack?
Cada llamada a función crea un stack frame (como vimos con el componente interactivo del artículo #1):
- Los parámetros se copian al stack frame (si son value types)
- Las variables locales se alocan en el stack frame
- Al retornar, el stack frame se destruye
Function types y el heap
Cuando almacenas una función en una variable (como var mathFunction: (Int, Int) -> Int = addTwoInts), la variable en el stack almacena un puntero a la función en __TEXT. Eso es solo 8 bytes — un puntero.
Pero cuando una función captura contexto (una nested function que usa variables de su scope padre), Swift necesita algo más complejo: un closure context en el heap. Esto es lo que veremos en detalle en el artículo #6.
inout y la optimización del compilador
func increment(_ value: inout Int) { value += 1}Semánticamente es copy-in/copy-out. Pero el compilador genera:
- Pass-by-reference cuando puede demostrar que no hay aliasing
- Copy-in/copy-out real solo cuando el argumento no tiene una dirección estable (por ejemplo, una computed property o un subscript)
La exclusivity rule de Swift (un solo acceso a una variable a la vez — artículo #17) es lo que hace posible esta optimización — el compilador puede asumir acceso exclusivo gracias a las reglas del lenguaje.

- Código de la función →
__TEXTsegment (de solo lectura, compartido) - Parámetros value type → Stack (se copian al frame)
- Variables locales → Stack (se destruyen al retornar)
- Function type en variable → Puntero en stack (8 bytes)
- Función que captura contexto → Heap (closure context) — artículo #6
- inout → Pass-by-reference optimizado cuando no hay aliasing
El compilador como tu aliado
El compilador de Swift puede hacer cosas impresionantes con funciones:
- Inlining: si la función es pequeña, el compilador copia su código directamente en el call site, eliminando el overhead de la llamada
- Constant folding: si los argumentos son constantes conocidas en compilación, el compilador puede calcular el resultado sin generar una llamada
- Dead code elimination: si el resultado de una función no se usa, el compilador puede eliminar la llamada por completo (si no tiene side effects — no imprime, no muta estado externo, ni hace nada observable más allá de retornar un valor)
- Specialization: para funciones genéricas (los generics — cubiertos en un artículo posterior), el compilador genera versiones especializadas para cada tipo concreto
Una función no es solo código — es un contrato con el compilador. Los tipos de parámetros, el tipo de retorno, la ausencia de side effects: todo eso es información que el compilador usa para optimizar.
Recapitulación
Hoy cubrimos todo sobre funciones en Swift:
- Definición básica —
func, parámetros, retorno, implicit return - Parámetros — sin parámetros, múltiples parámetros, retorno con tuplas, optional tuple return
- Argument labels — dos nombres (label + parameter),
_para omitir, legibilidad tipo oración - Defaults y variadic — valores por defecto,
...para número variable de argumentos - inout — modificar valores externos,
&explícito, pass-by-reference optimizado - Function types —
(Int, Int) -> Int, funciones en variables, como parámetros, como retorno - Nested functions — funciones dentro de funciones, preview de closures
- Memoria — código en
__TEXT, parámetros en stack, function pointers, inlining
Lo que viene
El siguiente artículo es uno de los más importantes de toda la serie: Closures. Vamos a explorar qué pasa cuando una función captura variables de su entorno — por qué eso requiere el heap, cómo funcionan las capture lists, qué significa @escaping, y por qué entender closures es la clave para dominar SwiftUI, Combine, y cualquier API moderna de Apple.
Si las funciones son ciudadanos de primera clase, los closures son su forma más poderosa.
Nos vemos la próxima semana.
Las funciones en Swift no son solo herramientas para organizar código — son la unidad fundamental de abstracción. Cuando entiendes que una función es un valor, empiezas a pensar en Swift como Swift fue diseñado para ser pensado.
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #11: Opcionales y Optional Chaining
El antídoto de Swift al error de mil millones de dólares. Optional es solo un enum — y nil, if let, guard let, ??, ! y el optional chaining compilan a preguntar en qué case estás.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #10: Herencia e Inicialización
Subclassing, overriding y super. Designated vs convenience initializers, two-phase initialization, failable y required init, y deinit — el ciclo de vida completo de una class, explicado en memoria.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #9: Propiedades, métodos y subscripts
Stored vs computed properties, observers, lazy, static. Cómo las propiedades definen el layout en memoria y por qué computed = zero storage.