Swift – Funciones

En esta ocación aprenderemos sobre funciones, uno de los pilares dentro del lenguaje Swift y de las características más flexibles.

Las funciones en Swift no solo configuran un bloque de código sino que también pueden fungir como un tipo de dato. Un tipo de dato que podemos pasar como parámetro o devolver como retorno através de otras funciones.

No solo esto, las funciones se convierten en métodos cuando las integramos dentro de una clase o estructura, y como si fuese poco también se pueden anidar. 🤯

¡Entremos en materia!

¿Qué es una función?

Una función es un segmento de código independiente al cual simplificamos una tarea específica. Es decir que una función bien implementada ejecuta solamente una tarea y no más.

La definición de una función es bien sencilla:

func rectanglePerimeter(length: Double, width: Double) -> Double {
    
    let perimeter = 2 * (length + width)
    
    return perimeter

} // rectanglePerimeter

La palabra clave func le indica al compilador que estamos definiendo una función, seguido a esto declaramos el nombre de la misma, en este caso rectanglePerimeter.

Dentro de los parentesis es donde establecemos los parametros que recibirá la función, en nuestro ejemplo son dos, el largo y el ancho de un rectangulo.

Al final de los parentesis tenemos la flecha que indica al compilador que vamos a definir un retorno y seguido a esta el tipo de dato de dicho retorno, en nuestro caso será de tipo Double. Las llaves definen el código asociado a esta función.

Organizar el código

Las funciones al igual que las clases y las estructuras son un método bien efectivo a la hora de organizar el código. Resultaría en un caos horrible tener toda la lógica de nuestro programa de manera consecutiva, y cada vez que vayamos a ubicar donde calculamos el perímetro ponernos a hacer scroll por todo el código.

Pero también las funciones nos ayuda a reutilizar código. Imaginemos que en nuestra aplicación tenemos que definir el algoritmo para clacular el perimetro cada vez que el usuario necesite de esta infomación. ¿No les parece algo innecesario?

No solo es cuestión de ahorrar tiempo y optimizar nuestro trabajo que es primordial, es que si tenemos que corregir un error en la formula del perímetro por ejemplo, tendríamos que ir por todo el código ubicándola y corriegiéndola sin introducir un nuevo error. Trabajar de esta manera es de cavernicolas y extremadamente absurdo.

Es absurdo porque existen mejores opciones. Gracias a las funciones podemos declarar el código de una funcionalidad una sola vez y reutilizarlo donde quiera que lo necesitemos. No hay necesidad de duplicar, o triplicar lo mismo cada vez que sea requerido.

Analogía

Una función viene a ser como una herramienta que utilizamos de manera recurrente. Dígase un microondas, cuya única función es cocinar o calentar el contenido que introducimos en él. Cuenta con una interfaz a través de la cual le pasamos parámetros y lo reutilizamos cada vez que es necesario.

¿Qué significa esto último?

Pues que en un principio cuando surge la necesidad vamos a la tienda y compramos un microondas. En este equipo (objeto y herramienta) se consolida su funcionalidad, y cada vez que lo usamos lo hacemos como un todo, como el sistema que representa: lo enchufamos a la corriente, introducimos la comida, interactuamos con su interfaz, etc.

En un microondas el magnetrón, la guía de onda o el plato de cocción sería lo homólogo al código que definimos dentro de una función, y la interfaz de usuario sería el equivalente a los parámetros que pasamos a la función.

¿Un solo objetivo?

Una función debe perseguir un solo objetivo, debe implementar una sola funcionalidad. Un martillo tiene una sola funcionalidad, no puede fungir como regadera.

Esta sencillez se traduce en un objeto fácil de gestionar. ¿Se imaginan un martillo regadera?

Cuando le echamos agua se vuelve absurdamente pesado por la cabeza que sería masisa en un 90% ya que hay un 10% que correspondería a los agujeros por donde sale el agua, mientras que el mango ya no sería de madera, ahora es de una especie de silicona y es hueco, en la punta inferior del mango tenemos una apertura por donde introducimos el agua.

Esto sería el ejemplo de una función que implementa varias funcionalidades y para colmo no relacionadas. ¿Cuál es el problema, a mi me gusta el martillo regadera? 😅

El problema reside en la manufactura, es absurdamente complejo estar fundiendo la cabeza de metal y sus agujeros, el mango huevo de silicona genera un desequilibrio en el peso del martillo ya que incluso teniendo agua la cabeza pesará mucho más y resultará muy incomodo de manejar.

A la hora de usarlo como regadera dónde lo único importante sería un contenedor de agua y los agujeros como cuello de botella y dosificador, tener que cargar con el peso de la cabeza cuándo regamos el jardín es completamente inútil.

Veamos un ejemplo similar pero ahora en código:

func rectanglePerimeter(length: Double, width: Double) {
    
    let perimeter = 2 * (length + width)
    
    print("El perimetro del rectángulo es de \(perimeter) unidades.")

} // rectanglePerimeter

rectanglePerimeter(length: 50, width: 25)

Aquí tenemos una versión de la función rectanglePerimeter. En esta ocasión solo toma los parámetros de entrada y no devuelve ningun valor, ejecuta los calculos y los imprime en pantalla.

¿Qué hay de malo con esta función? ¡Hemos reducido el código!

El problema es teórico, el código compila pero a nivel de arquitectura no es ni de lejos una implementación ideal. No solo calcula el perímetro, también imprime un mensaje fijo con el resultado.

Una función tanto como una estructura o clase debe ser todo terreno, debe hacer su trabaja en cualquier escenario. Si esta función fuera parte de un framework no pudiéramos modificar su implementación y por ende no pudiéramos capturar su valor, y siempre imprimiría el mensaje aún cuando no sea necesario.

Aún cuando la función es nuestra y podemos modificar el código, tendremos que tener mucho cuidado ya que en dependencia de las modificaciones tendremos luego que ir actualizando el código en cada lugar donde la hemos usado.

Una implementación o intenciones de mejora en plan el martillo regadera pudiera ser:

func rectanglePerimeter(length: Double, width: Double, mute: Bool = true) -> Double {
    
    let perimeter = 2 * (length + width)
    
    if !mute {
        
        print("El perimetro del rectangulo es de \(perimeter) unidades.")
        
    } // if
    
    return perimeter

} // rectanglePerimeter

let perimeter = rectanglePerimeter(length: 50, width: 25)

print("Necesita comprar \(perimeter) unidades de cerca para su terreno.")

rectanglePerimeter(length: 50, width: 25, mute: false)

En esta versión hemos restablecido el valor de retorno, y hemos agregado un nuevo parámetro con un valor por defecto asociado, así evitamos que se rompa nuestro código si es que hemos usado esta función en otras partes del código.

Si la variable booleana mute es true el mensaje no se muestra, y en caso contrario sí. De esta manera podemos controlar esta característica, tal y como hemos hecho en la última línea.

Esta implementación es horrible y completamente innecesaria. Ahora, me gustaría puntualizar algo en la última línea:

Mírenla, si usted no ha sido el desarrollador de la función ¿pudiera inferir de que va el último parámetro?

¿Mute; silencio, en una función cuyo nombre es rectanglePerimeter y del cual sí podemos inferir su función? 🤔

Aunque pongamos muteOutput o muteMessage, cualquiera se preguntaría:

¿Silenciar la salida? ¿Cuál mensaje?

Por esto es que un buen nombre es siempre tan importante, en nuestro caso el propio nombre establece un contexto semántico donde salta a la vista el último parámetro.

Este contraste es debido a que este parámetro sobra básicamente, es algo forzado que configura más un parche a un error de diseño, que una parte necesaria de dicha funcionalidad.

func rectanglePerimeter(length: Double, width: Double) -> Double {
    
    let perimeter = 2 * (length + width)

    return perimeter

} // rectanglePerimeter

let perimeter = rectanglePerimeter(length: 50, width: 25)

print("Necesita comprar \(perimeter) unidades de cerca para su terreno.")

// En otro lugar del código

let fenceUnitsSold = rectanglePerimeter(length: 250, width: 430)

En este ejemplo final hemos simplificado la función basado en lo que estamos comentando. Le pasamos los valores a calcular y esta nos devuelve el resultado del mismo tipo que los parámetros de entrada.

Si luego este valor se desea convertir a Int o a String es algo que se debe manejar de manera particular en la sección del código donde esta necesidad surja.

Tal como en la última línea donde la utilizamos para almacenar en nuestro registro de ventas, en la base de datos, las unidades de cerca que hemos vendido.

Como podemos observar esta versión es mucho más simple y flexible, a la función le da igual el contexto externo en que la estamos usando o que hagamos luego con el valor de retorno, hace lo que tiene que hacer y lo hace bien.

Luego de esta extensa, y creo que bastante completa, introducción pasemos a ver las funciones en más detalles.

Parámetros de Funciones y Valores de Retorno

Los parámetros y las funciones de retorno de una función son bien flexibles en Swift. Podemos definir desde una función utilitaria simple y con un solo parámetro sin nombre, hasta una función compleja con nombres de parámetros expresivos y diferentes opciones.

Parámetros

Los parámetros de una función son una interfaz para los clientes de la misma. Cada vez que la usamos mediante los parámetros establecemos los valores necesarios para que esta funcione tal y como deseamos.

Ahora, no es obligatorio que especifiquemos parámetros de entrada (como también se les llama) a una función. Lo hacemos solo si tiene sentido, por ejemplo si definimos una función que devuelva el número pi:

func pi() -> Double {
    
    return Double.pi
    
} // pi

print("La constante π es igual a: \(pi())")

no necesitamos definir parámetros, ya que la función no necesita de datos externos para obtener este número. Básicamente porque es una constante y ya viene definida en el tipo de dato Double.

La salida en pantalla es:

La constante π es igual a: 3.141592653589793

La definición de esta función aún necesita paréntesis luego del nombre, incluso cuando no recibe parámetros. La función también es precedida por estos paréntesis vacíos cuando es llamada, como por ejemplo en la última línea donde pasamos su valor de retorno a la función print.

Múltiples Parámetros

Las funciones pueden tener múltiples parámetros de entrada, estos se definen separados por una coma y dentro de los paréntesis que siguen al nombre.

El siguiente ejemplo muestra una función que toma dos parámetros, el nombre de una persona y un valor booleano en representación de si es o no su cumpleaños.

func sayHello(name: String, isBirthday: Bool = false) -> String {
    
    if isBirthday {
        
        return "🎊 ¡Hola \(name), feliz cumpleaños! 🎉"
        
    } else {
        
        return "👋 ¡Hola \(name), feliz día! 🌞"
        
    } // else
    
} // sayHello

let userName = "Paco"

print(sayHello(name: userName, isBirthday: true))

La salida en pantalla sería:

🎊 ¡Hola Paco, feliz cumpleaños! 🎉

El ejemplo es bastante sencillo, esta función se encarga de establecer un mensaje de saludo, y en caso de ser el cumpleaños de la persona pues lo saludamos de una manera distinta.

Múltiples Valores de Retorno

Como programadores nos encontraremos ante disimiles situaciones y matices, y como parte de esta realidad eventualmente necesitaremos de una función que nos devuelva varios valores. Ojo, que no hablo de un valor distinto en cada llamada, ¡no! me refiero a varios en una sola llamada.

En Swift esto lo logramos mediante una tupla. Es decir, definimos una función que devuelva una tupla como valor de retorno; de esta manera podemos enviar multiples valores en una sola orden de retorno.

func minMax(values: [Int]) -> (min: Int, max: Int) {
    
    var currentMin = values[0]
    var currentMax = values[0]
    
    for value in values {

        if value < currentMin {
            
            currentMin = value
            
        } else if value > currentMax {
            
            currentMax = value
            
        } // else if
        
    } // for
    
    return (currentMin, currentMax)
    
} // minMax

let examScores = [42, 54, 85, 73, 96]

let minMaxScores = minMax(values: examScores)

print("El nota más baja de la clase fue de \(minMaxScores.min) puntos y la más alta de \(minMaxScores.max) puntos.")

Esta función nos ayuda a encontrar el número más pequeño y el más grande dentro del arreglo que recibe como parámetro. El valor de retorno de la función es una tupla de tipo (min: Int, max: Int).

Valores de Retorno Opcionales

¿Qué sucedería si existiera una posibilidad de que la función no devolviera un valor? ¿Qué hacemos si ante cierto evento no hubiera un resultado a retornar?

En estos casos lo que hacemos es retornar un valor opcional, para esto colocamos el signo de interrogación al final de la declaración de retorno, de esta manera: Double?, (Int, Int)? o (String, Int, Bool)? según sea el caso.

Tomando como referencia la función minMax, imaginemos ahora que le pasamos un array vacio.

let examScores = [Int]()

let minMaxScores = minMax(values: examScores)

Esto causaría un error en tiempo de ejecución, específicamente:

Fatal error: Index out of range

Ya que esto puede suceder, lo más lógico sería validar los datos de entrada, si el array tiene valores continuamos con la ejecución del código, de lo contrario devolvemos nil.

func minMax(values: [Int]) -> (min: Int, max: Int)? {
    
    if values.isEmpty {
        
        return nil
        
    } // if
    
    var currentMin = values[0]
    var currentMax = values[0]
    
    for value in values {

        if value < currentMin {
            
            currentMin = value
            
        } else if value > currentMax {
            
            currentMax = value
            
        } // else if
        
    } // for
    
    return (currentMin, currentMax)
    
} // minMax

//let examScores = [42, 54, 85, 73, 96]

let examScores = [Int]()

if let minMaxScores = minMax(values: examScores) {
    
    print("ℹ️ El nota más baja de la clase fue de \(minMaxScores.min) puntos y la más alta de \(minMaxScores.max) puntos.")
    
} else {
    
    print("⚠️ ¡No hay notas que analizar!")
    
} // else

En este ejemplo hemos añadido varios cambios. La función ahora devuelve un valor opcional, en dependencia de si el arreblo está vacío o contiene elementos. Esto último es lo primero que validamos mediante un if y la propiedad boleana isEmpty.

Luego cuando hacemos uso de la función nos apoyamos en un bloque if-let para convertir la tupla de un opcional a una normal, en caso contrario el array está vacío y se ejecuta el bloque else.

Nombres Externos e Internos de Parámetros

Los parámetros de las funciones tienen un nombre de parámetro externo y uno local. 🤯

El nombre externo de un parámetro es usado para etiquetar los valores que se pasan a la función cuando hacemos una llamada a la misma. Por su parte el nombre local es usado dentro del ambito de la función, internamente.

func sayHello(to name: String, isBirthday birthday: Bool = false) -> String? {
    
    if name.isEmpty {
        
        return nil
        
    } // if
    
    if birthday {
        
        return "🎊 ¡Hola \(name.capitalized), feliz cumpleaños! 🎉"
        
    } else {
        
        return "👋 ¡Hola \(name.capitalized), feliz día! 🌞"
        
    } // else
    
} // sayHello

let userName = "carlos"

if let greetingMessage = sayHello(to: userName, isBirthday: true) {
    
    print(greetingMessage)
    
} else {
    
    print("¡No hay a quien saludar! 🤷‍♂️")
    
} // else

En este ejemplo podemos ver como hemos cambiado la firma de la función, de:

sayHello(name: String, isBirthday: Bool = false) -> String

hacia:

sayHello(to name: String, isBirthday birthday: Bool = false) -> String?

Los nombres de los parámetros, de izquierda a derecha, el primero es el parámetro externo, el que usamos a la hora de llamar la función; el segundo es el que usamos dentro del ámbito de la misma.

La lógica de esto reside en la comodidad y en la legibilidad, tanto para el desarrollador de la función como para el que luego la usa.

Por ejemplo, para algunos a nivel de uso resulta más legible:

sayHello(to: "Maritza", isBirthday: true)

que:

sayHello(name: "Maritza", isBirthday: true)

La primera se leería: «Saluda a Maritza ¿es su cumpleaños?» mientras que la última sería algo al estilo: «Saluda a alguien de nombre Maritza ¿es su cumpleaños?«.

Nota: Digo que esta sería una mejor opción para algunos ya que es una cuestión bastante relativa a cada cual. Swift es lo suficientemente flexible para que cada cual adopte el enfoque de su agrado y el código resultante sea lo más expresivo posible.

Ahora, dentro del ámbito de la función usar la constante to o isBirthday no resultaría cómodo y menos aún necesario, ya que dentro de esta sabemos perfectamente que estamos recibiendo dos valores.

El nombre de la persona y el estado de su cumpleaños, de ahí viene el nombre interno, el que especificamos a la derecha del nombre externo. Estos son los que usamos dentro de la función, to pasa a ser name y isBirthday se convierte en birthday.

Parámetros sin nombre externo

Pero también hay ocaciones donde el nombre de la función es más que suficiente para establecer un contexto. Por lo que en estos casos tener un nombre externo puede resultar en algo redundante.

func showMessage(_ message: String) {
    
    print(message)
    
} // showMessage

showMessage("Hola!")

Lo único que tenemos que hacer es sustituir el nombre externo por un guión bajo, tal y como pueden ver en el ejemplo. Por otra parte también pudiera darse el caso de una función con dos parámetros o más, donde algunos tiene nombre externo y otros no.

enum MessageType {
    
    case Info
    case Warning
    
} // MessageType

func showMessage(_ message: String, type: MessageType) {
    
    switch type {
        
    case .Info:
        
        print("Info: \(message)")
        
    case .Warning:
        
        print("Warning: \(message)")
        
    } // switch

} // showMessage

showMessage("¡Conexión intermitente!", type: .Warning)

En este ejemplo matizamos el mensaje con el tipo de información que mostraremos, el parámetro que recibe el mensaje no tiene nombre externo ya que se infiere por el contexto, mientras que el último parámetro si cuenta con uno.

Valores por Defecto

Como hemos visto en la introducción, a las funciones en Swift también le podemos especificar valores por defecto a los parámetros que esta recibe.

Esto lo hacemos especificando el valor luego del tipo de dato, aquí un ejemplo que acabamos de ver:

sayHello(to name: String, isBirthday birthday: Bool = false) -> String?

donde el parámetro birthday tiene un valor por defecto de false.

¿Por qué? Bueno pues porque la mayor parte del tiempo este parámetro se usaría con el valor false, porque solo cumplimos año una vez al año.

¿Qué sentido tiene estar constantemente especificando false en el segundo parámetro?

Con el parámetro por defecto lo hacemos opcional, y por ende solo se utilizará cuando sea el cumpleaños de la persona.

Función Variádica

Las funciones variádicas son muy útiles, nos brindan una flexibilidad realmente de aplausos, pero ¿Qué es una función variádica?

Una función variádica es aquella que acepta cero o más parámetros de un tipo específico. Establecemos un parámetro como variádico cuando el número de valores individuales que necesitamos pasar es indefinido.

La declaración es bien simple, solo tenemos que agregar tres puntos al final del tipo de dato del parámetro en cuestión. Ejemplo:

func averageValue(of numbers: Double...) -> Double {
    
    var total: Double = 0
    
    for number in numbers {
        
        total += number
        
    } // for
    
    let average = total / Double(numbers.count)
    
    return average
    
} // averageValue

print("El promedio de los valores: 1, 2, 7, 4 y 5 es: \(averageValue(of: 1, 2, 7, 4, 5))")

print("El promedio de los valores: 3, 8.25 y 18.75 es: \(averageValue(of: 3, 8.25, 18.75))")

La función calcula el promedio de una serie de valores que pasamos como parametros, la cantidad de valores que podemos pasar a la función no tiene un límite específico, puede recibir 3 elementos, 5 o más.

La salida en pantalla sería:

El promedio de los valores: 1, 2, 7, 4 y 5 es: 3.8
El promedio de los valores: 3, 8.25 y 18.75 es: 10.0

Parámetros In-Out

Cuando marcamos el parámetro de una función como In Out lo que estamos estableciendo es lo siguiente:

A la hora de llamar a la función y pasar una variable como parámetro de la misma, en lugar de acceder a una copia del valor asociado a esta variable, lo que recibiremos será un valor por referencia, es decir que recibiremos una referencia a la dirección de memoria, un puntero de la variable que pasamos como parámetro.

Esto lo que permite, básicamente, es que nuestros cambios sobre esta dirección de memoria persistan luego de finalizada la ejecución de la función.

Establecer un parámetro como In Out es bien sencillo, basta con poner el modifcador inout antes del tipo de dato de dicho parámetro.

func swapValue(from a: inout Int, to b: inout Int) {
    
    let tempA = a
    
    a = b
    
    b = tempA
    
} // swapValue

var value1 = 10
var value2 = 20

print("El valor 1 es igual a: \(value1) y 2 es igual a: \(value2)")

swapValue(from: &value1, to: &value2)

print("El valor 1 es igual a: \(value1) y 2 es igual a: \(value2)")

Al llamar una función cuyos parámetros son inout tenemos que especificar el signo & antes del nombre de la variable.

Como la lógica aquí reside en que los cambios persistan, es más que evidente que solo podemos pasar variables. Tampoco pueden ser valores literales ya que estos no pueden ser modificados, estos parámetros no pueden tener valores por defecto ni pueden ser variádicos.

La salida en pantalla del código anterior es:

El valor 1 es igual a: 10 y 2 es igual a: 20
El valor 1 es igual a: 20 y 2 es igual a: 10

Función anidada

Una función anidada es aquella que ha sido definida dentro del cuerpo de otra. Las funciones anidadas están ocultas al exterior de la función o dicho de otra manera su ámbito está restringido al cuerpo de la función donde esta fue creada y solamente dentro de este podrá ser llamada.

Pero como ya vimos las funciones pueden retornar funciones, por lo que en caso de ser necesario una función anidada puede extender su ámbito al ser retornada por su función padre. Veamos el ejemplo anterior con algunas modificaciones:

func chooseStepFunction(_ backwards: Bool) -> (Int) -> Int {
    
    func stepForward(_ input: Int) -> Int {
        
        return input + 1
        
    } // stepForward
    
    func stepBackward(_ input: Int) -> Int {
        
        return input - 1
        
    } // stepBackward
    
    return backwards ? stepBackward : stepForward
    
} // chooseStepFunction

var currentValue = 10

let moveNearerToZero = chooseStepFunction(currentValue > 0)

print("Contando hasta cero:\n")

while currentValue != 0 {
    
    print("\(currentValue)... ")
    
    currentValue = moveNearerToZero(currentValue)
    
} // while

print("\ncero! 😅")

la salida en pantalla sería:

Contando hasta cero:

10... 
9... 
8... 
7... 
6... 
5... 
4... 
3... 
2... 
1... 

cero! 😅

Conclusiones

Bueno amigos, no creo que haya que aportar mucho más sobre las funciones. Creo que ha quedado bastante clara su utilidad y cuan importante es dentro del lenguaje Swift.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!