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. El compilador genera variantes de la función para cada combinación — es compile-time sugar, sin overhead en runtime.
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
Pruébalo tú mismo — asigna diferentes funciones a la misma variable, ajusta los argumentos y observa cómo el resultado cambia pero el tipo siempre es (Int, Int) -> Int:
Funciones como valores
Asigna diferentes funciones a la misma variable y observa cómo cambia el resultado.
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 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 variables del scope que la rodea. 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 hay acceso concurrente o aliasing potencial
La exclusivity rule de Swift (que veremos en el 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)
- Specialization: para funciones genéricas, 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
- ios
- performance
Dominando Instruments (Parte 3): método científico, Time Profiler avanzado y profiling a escala
Aprende a diagnosticar problemas de rendimiento como un proceso científico. Domina Weight vs Self-Weight, Charge/Prune/Flatten, y escala tu profiling con xctrace.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #4: Control de flujo — de if/else a pattern matching
if/else, switch exhaustivo con pattern matching, guard como filosofía, y cómo el compilador optimiza tus decisiones a jump tables.
-
- swift
- ios
- performance
Dominando Instruments (Parte 2.5): malloc, free y ARC — cómo funciona la memoria bajo el capó
Entiende visceralmente qué pasa cuando tu código se ejecuta. Visualiza malloc, free, reference counting y retain cycles con componentes interactivos.