Una peque帽a pr谩ctica de programaci贸n funcional en Swift para principiantes



Me gustar铆a presentar el concepto de Programaci贸n Funcional a los principiantes de la manera m谩s simple, destacando algunas de sus ventajas de las muchas otras que realmente har谩n que el c贸digo sea m谩s legible y expresivo. Recog铆 algunas demostraciones interesantes para ti que est谩n en Playground en Github .

Programaci贸n funcional: definici贸n


En primer lugar, la programaci贸n funcional no es un lenguaje o una sintaxis, sino una forma de resolver problemas dividiendo procesos complejos en procesos m谩s simples y su posterior composici贸n. Como su nombre lo indica, " Programaci贸n funcional " , la unidad de composici贸n para este enfoque es una funci贸n ; y el prop贸sito de tal funci贸n es evitar el cambio de estado o valores fuera de su scope) .

En Swift World, existen todas las condiciones para esto, porque las funciones aqu铆 son participantes tan completos en el proceso de programaci贸n como los objetos, y el problema de la mutation se resuelve a nivel del concepto de TIPOS de value ( struct estructuras y enum enumeraci贸n) que ayudan a gestionar la mutabilidad ( mutation ) y comunicar claramente c贸mo y cu谩ndo puede suceder esto.

Sin embargo, Swift no Swift , en el sentido completo, el lenguaje de la programaci贸n funcional , no lo obliga a la programaci贸n funcional , aunque reconoce las ventajas de los enfoques funcionales y encuentra formas de incorporarlos.

En este art铆culo, nos centraremos en utilizar los elementos integrados de la Programaci贸n funcional en Swift (es decir, "listos para usar") y en comprender c贸mo puede utilizarlos c贸modamente en su aplicaci贸n.

Enfoques imperativos y funcionales: comparaci贸n


Para evaluar el Enfoque Funcional , comparemos las soluciones a un problema simple de dos maneras diferentes. La primera soluci贸n es " Imperativo " , en el que el c贸digo cambia el estado dentro del programa.

 //Imperative Approach var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for i in 0..<numbers.count { let timesTen = numbers[i] * 10 numbers[i] = timesTen } print(numbers) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Observe que manipulamos los valores dentro de la matriz mutable llamada numbers y luego la imprimimos en la consola. Mirando este c贸digo, intente responder las siguientes preguntas que discutiremos en un futuro cercano:

  1. 驴Qu茅 est谩s tratando de lograr con tu c贸digo?
  2. 驴Qu茅 sucede si otro thread intenta acceder a la matriz de numbers mientras se est谩 ejecutando su c贸digo?
  3. 驴Qu茅 sucede si desea tener acceso a los valores originales en la matriz de numbers ?
  4. 驴Qu茅 tan confiable se puede probar este c贸digo?

Ahora veamos un enfoque " funcional " alternativo:

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] extension Array where Element == Int { func timesTen() -> [Int] { var output = [Int]() for num in self { output.append(num * 10) } return output } } let result = numbers.timesTen() print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

En este fragmento de c贸digo, obtenemos el mismo resultado en la consola, abordando la soluci贸n del problema de una manera completamente diferente. Tenga en cuenta que esta vez nuestra matriz de numbers es inmutable gracias a la palabra clave let . Hemos movido el proceso de multiplicar n煤meros de la matriz de numbers al m茅todo timesTen() , que se encuentra en la extensi贸n de extension la Array . Todav铆a usamos un bucle for y modificamos una variable llamada output , pero el scope esta variable est谩 limitado solo por este m茅todo. De manera similar, nuestro argumento de entrada self se pasa al m茅todo timesTen() por valor ( by value ), teniendo el mismo alcance que la variable de output . Se timesTen() m茅todo timesTen() , y podemos imprimir en la consola tanto la matriz de numbers originales como el resultado de la matriz de result .
Volvamos a nuestras 4 preguntas.

1. 驴Qu茅 est谩s tratando de lograr con tu c贸digo?

En nuestro ejemplo, realizamos una tarea muy simple multiplicando los n煤meros en la matriz de numbers por 10 .

Con un enfoque imperativo , para obtener una salida, debe pensar como una computadora, siguiendo las instrucciones en el bucle for . En este caso, el c贸digo muestra lograr el resultado. Con el enfoque funcional , " " se "envuelve" en el m茅todo timesTen() . Siempre que este m茅todo se haya implementado en otro lugar, en realidad solo puede ver la expresi贸n numbers.timesTen() . Dicho c贸digo muestra claramente logra con este c贸digo y no se resuelve la tarea. Esto se llama programaci贸n declarativa , y es f谩cil adivinar por qu茅 este enfoque es atractivo. El enfoque imperativo hace que el desarrollador entienda c贸digo para determinar debe hacer. El enfoque funcional en comparaci贸n con el enfoque imperativo es mucho m谩s "expresivo" y le brinda al desarrollador una oportunidad de lujo de simplemente asumir que el m茅todo hace lo que dice hacer. (Obviamente, esta suposici贸n se aplica solo al c贸digo previamente verificado).

2. 驴Qu茅 sucede si otro thread intenta acceder a la matriz de numbers mientras se ejecuta el c贸digo?

Los ejemplos presentados anteriormente existen en un espacio completamente aislado, aunque en un entorno complejo de subprocesos m煤ltiples, es muy posible que dos threads intenten acceder a los mismos recursos simult谩neamente. En el caso del enfoque Imperativo , es f谩cil ver que cuando otro thread tiene acceso a la matriz de numbers en el proceso de uso, el resultado ser谩 dictado por el orden en que los threads acceden a la matriz de numbers . Esta situaci贸n se denomina race condition y puede conducir a un comportamiento impredecible e incluso a la inestabilidad y el bloqueo de la aplicaci贸n.

En comparaci贸n, el enfoque funcional no tiene "efectos secundarios". En otras palabras, la salida del m茅todo de output no cambia ning煤n valor almacenado en nuestro sistema y est谩 determinada 煤nicamente por la entrada. En este caso, cualquier subproceso ( threads ) que tenga acceso a la matriz de numbers SIEMPRE recibir谩 los mismos valores y su comportamiento ser谩 estable y predecible.

3. 驴Qu茅 sucede si desea tener acceso a los valores originales almacenados en la matriz de numbers ?

Esta es una continuaci贸n de nuestra discusi贸n sobre los "efectos secundarios". Obviamente, los cambios de estado no se rastrean. Por lo tanto, con el enfoque imperativo , perdemos el estado inicial de nuestra matriz de numbers durante el proceso de conversi贸n. Nuestra soluci贸n, basada en el enfoque funcional , guarda la matriz de numbers originales y genera una nueva matriz de result con las propiedades deseadas en la salida. Deja la matriz de numbers original intacta y adecuada para el procesamiento futuro.

4. 驴Qu茅 tan confiable puede ser probado este c贸digo?

Dado que el enfoque funcional destruye todos los "efectos secundarios", la funcionalidad probada est谩 completamente dentro del m茅todo. La entrada de este m茅todo NUNCA experimentar谩 ning煤n cambio, por lo que puede probar varias veces usando el ciclo tantas veces como desee, y SIEMPRE obtendr谩 el mismo resultado. En este caso, la prueba es muy f谩cil. En comparaci贸n, probar la soluci贸n Imperativa en un bucle cambiar谩 el inicio de la entrada y obtendr谩 resultados completamente diferentes despu茅s de cada iteraci贸n.

Resumen de beneficios


Como vimos en un ejemplo muy simple, el Enfoque Funcional es genial si se trata de un Modelo de Datos porque:

  • Es declarativo
  • Soluciona problemas relacionados con hilos como race condition y puntos muertos
  • Deja el estado sin cambios, que se puede utilizar para transformaciones posteriores.
  • Es f谩cil de probar.

Avancemos un poco m谩s en el aprendizaje de la programaci贸n funcional en Swift . Se supone que los principales "actores" son funciones, y deber铆an ser principalmente objetos de la primera clase .

Funciones de primera clase y funciones de orden superior


Para que una funci贸n sea de primera clase, debe tener la capacidad de ser declarada como variable. Esto le permite administrar la funci贸n como un TIPO de datos normal y, al mismo tiempo, ejecutarla. Afortunadamente, en Swift funciones son objetos de la primera clase, es decir, se admiten pas谩ndolas como argumentos a otras funciones, devolvi茅ndolas como resultado de otras funciones, asign谩ndolas a variables o almacen谩ndolas en estructuras de datos.

Debido a esto, tenemos otras funciones en Swift : funciones de orden superior que se definen como funciones que toman otra funci贸n como argumento o devuelven una funci贸n. Hay muchos de ellos: map , filter , reduce , para cada forEach , flatMap , compactMap , sorted , etc. Los ejemplos m谩s comunes de funciones de orden superior son map , filter y reduce . No son globales, todos est谩n "unidos" a ciertos TIPOS. Funcionan en todos los TIPOS de Sequence , incluida la Collection , que est谩 representada por estructuras de datos Swift como una Array , un Dictionary y un Set . En Swift 5 , las funciones de orden superior tambi茅n funcionan con un TIPO - Result completamente nuevo.

map(_:)


En el map(_:) Swift map(_:) toma una funci贸n como par谩metro y convierte los valores de un determinado acuerdo con esta funci贸n. Por ejemplo, al aplicar map(_:) a una matriz de valores de Array , aplicamos una funci贸n de par谩metro a cada elemento de la matriz original y obtenemos una matriz de Array , pero tambi茅n los valores convertidos.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] func timesTen(_ x:Int) -> Int { return x * 10 } let result = numbers.map (timesTen) print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

En el c贸digo anterior, creamos la funci贸n timesTen (_:Int) , que toma un valor entero Int y devuelve el valor entero Int multiplicado por 10 , y lo usamos como par谩metro de entrada a nuestra funci贸n de map(_:) orden superior map(_:) , aplic谩ndolo a nuestra matriz numbers Obtuvimos el resultado que necesitamos en la matriz de result .

El nombre de la funci贸n de par谩metro timesTen para funciones de orden superior como map(_:) no importa, el par谩metro de entrada y el valor de retorno son importantes, es decir, la firma (Int) -> Int par谩metro de entrada de funci贸n. Por lo tanto, podemos usar funciones an贸nimas en el map(_:) - cierres - en cualquier forma, incluidos aquellos con nombres de argumentos acortados $0 , $1 , etc.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let result = numbers.map { $0 * 10 } print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Si miramos la funci贸n map(_ :) para una Array , podr铆a verse as铆:

 func map<T>(_ transform: (Element) -> T) -> [T] { var returnValue = [T]() for item in self { returnValue.append(transform(item)) } return returnValue } 

Este es un c贸digo imperativo que nos es familiar, pero ya no es un problema de desarrollador, es un problema de Apple , un problema de Swift . La implementaci贸n de la funci贸n map(_:) orden superior est谩 optimizada por Apple en t茅rminos de rendimiento, y nosotros, los desarrolladores, tenemos garantizada la funcionalidad map(_:) , por lo que solo podemos expresar correctamente con el argumento de la funci贸n de transform que queremos sin preocuparnos por se implementar谩. Como resultado, obtenemos c贸digo perfectamente legible en forma de una sola l铆nea, que funcionar谩 mejor y m谩s r谩pido.

 //Functional Approach let possibleNumbers = ["1", "2", "three", "///4///", "5"] let mapped = possibleNumbers.map {str in Int(str) } print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

El devuelto por la funci贸n de par谩metro puede no coincidir con el elementos en la colecci贸n original.

En el c贸digo anterior, tenemos posibles n煤meros enteros possibleNumbers , representados como cadenas, y queremos convertirlos a n煤meros enteros de Int , utilizando el inicializador failable Int(_ :String) representado por el cierre { str in Int(str) } . Hacemos esto usando map(_:) y obtenemos una matriz mapped de Optional como salida:



No convertir elementos de nuestra matriz possibleNumbers a enteros, como resultado, una parte recibi贸 el valor nil , lo que indica la imposibilidad de convertir la String a un entero Int , y la otra parte se convirti贸 en Optionals , que tienen valores:

 print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

compactMap(_ :)


Si la funci贸n de par谩metro pasada a la funci贸n de orden superior tiene un valor Optional en la salida, entonces puede ser m谩s 煤til usar otra funci贸n de orden superior, similar en significado: compactMap(_ :) , que hace lo mismo que map(_:) , pero adicionalmente "expande" los valores recibidos en la salida Optional y elimina los valores nil de la colecci贸n.



En este caso, obtenemos una matriz de compactMapped TYPE [Int] , pero posiblemente m谩s peque帽a:

 let possibleNumbers = ["1", "2", "three", "///4///", "5"] let compactMapped = possibleNumbers.compactMap(Int.init) print (compactMapped) // [1, 2, 5] 



Siempre que use el init?() Como la funci贸n de transformaci贸n, tendr谩 que usar compactMap(_ :) :

 // Validate URLs let strings = ["https://demo0989623.mockable.io/car/1", "https://i.imgur.com/Wm1xcNZ.jpg"] let validateURLs = strings.compactMap(URL.init) // Separate Numbers and Operations let mathString: String = "12-37*2/5+44" let numbers1 = mathString.components(separatedBy: ["-", "*", "+", "/"]).compactMap(Int.init) print(numbers1) // [12, 37, 2, 5, 44] 

Debo decir que hay razones m谩s que suficientes para usar la funci贸n de orden superior compactMap(_ :) . Swift "amores" Valores Optional , se pueden obtener no solo usando el failable " init?() failable ", sino tambi茅n usando el as? "Fundici贸n":

 let views = [innerView,shadowView,logoView] let imageViews = views.compactMap{$0 as? UIImageView} 

... y la try? al procesar errores arrojados por algunos m茅todos. Debo decir que a Apple preocupa que el uso de try? muy a menudo conduce al doble Optional y en Swift 5 ahora deja solo un nivel Optional despu茅s de aplicar try? .

Hay una funci贸n m谩s similar en nombre del flatMap(_ :) orden flatMap(_ :) , sobre el cual es un poco m谩s bajo.

A veces, para usar el map(_:) funciones de orden superior map(_:) , es 煤til usar el m茅todo zip (_:, _:) para crear una secuencia de pares a partir de dos secuencias diferentes.

Supongamos que tenemos una view en la que se representan varios puntos, conectados entre s铆 y formando una l铆nea discontinua:



Necesitamos construir otra l铆nea discontinua que conecte los puntos medios de los segmentos de la l铆nea discontinua original:



Para calcular el punto medio de un segmento, debemos tener las coordenadas de dos puntos: el actual y el siguiente. Para hacer esto, podemos crear una secuencia que consista en pares de puntos, el actual y el siguiente, utilizando el m茅todo zip (_:, _:) , en el que usaremos la matriz de puntos de inicio y la matriz de los siguientes points.dropFirst() :

 let pairs = zip (points,points.dropFirst()) let averagePoints = pairs.map { CGPoint(x: ($0.x + $1.x) / 2, y: ($0.y + $1.y) / 2 )} 

Teniendo tal secuencia, calculamos muy f谩cilmente los puntos medios usando el map(_:) funciones de orden superior map(_:) y los mostramos en el gr谩fico.

filter (_:)


En Swift , el filter (_:) funci贸n de orden superior filter (_:) est谩 disponible para la mayor铆a de los que est谩 disponible la funci贸n de map(_:) . Puede filtrar cualquier Sequence secuencia con filter (_:) , 隆esto es obvio! El m茅todo filter (_:) toma otra funci贸n como par谩metro, que es una condici贸n para cada elemento de la secuencia, y si la condici贸n se cumple, entonces el elemento se incluye en el resultado, y si no, no se incluye. Esta "otra funci贸n" toma un solo valor, un elemento de la Sequence secuencia, y devuelve un Bool , el llamado predicado.

Por ejemplo, para las matrices de matrices, el filter (_:) funci贸n de orden superior filter (_:) aplica la funci贸n de predicado y devuelve otra matriz que consta 煤nicamente de aquellos elementos de la matriz original para los cuales la funci贸n de predicado de entrada devuelve true .

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let filted = numbers.filter{$0 % 2 == 0} //[2, 4, 6, 8, 10] 

Aqu铆, el filter (_:) funci贸n de orden superior filter (_:) toma cada elemento de la matriz de numbers (representado por $0 ) y verifica si este elemento es un n煤mero par. Si este es un n煤mero par, entonces los elementos de la matriz de numbers caen en la nueva matriz filted , de lo contrario no. En forma declarativa, informamos al programa queremos obtener en lugar de preocuparnos por debemos hacerlo.

Dar茅 otro ejemplo del uso del filter (_:) funci贸n de orden superior filter (_:) para obtener solo los primeros 20 n煤meros de Fibonacci con valores < 4000 :

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] 

Obtenemos una secuencia de tuplas que consta de dos elementos de la secuencia de Fibonacci: el n-茅simo y (n + 1) -茅simo:

 (0, 1), (1, 1), (1, 2), (2, 3), (3, 5) 鈥 

Para un procesamiento adicional, limitamos el n煤mero de elementos a los veinti煤n elementos usando el prefix (20) y tomamos el elemento 0 de la tupla generada usando el map {$0.0 } , que corresponder谩 a la secuencia de Fibonacci que comienza con 0 :

 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Podr铆amos tomar el 1 elemento de la tupla formada usando el map {$0.1 } , que corresponder铆a a la secuencia de Fibonacci que comienza con 1 :

 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Obtenemos los elementos que necesitamos con la ayuda del filter {$0 % 2 == 0 && $0 < 4000} funci贸n de orden superior filter {$0 % 2 == 0 && $0 < 4000} , que devuelve una matriz de elementos de secuencia que satisfacen el predicado dado. En nuestro caso, ser谩 una matriz de enteros [Int] :

 [0, 2, 8, 34, 144, 610, 2584] 

Hay otro ejemplo 煤til del uso del filter (_:) para una Collection .

Me encontr茅 con un problema real , cuando tienes una gran variedad de images que se muestran con CollectionView , y con la tecnolog铆a Drag & Drop puedes recopilar un mont贸n de im谩genes y moverlas a todas partes, incluso soltarlas en " bote de basura ".



En este caso, la matriz de 铆ndices removedIndexes 铆ndices removedIndexes vertidas en el "bote de basura" son fijos, y usted necesita crear una nueva matriz de im谩genes, excluyendo aquellos cuyos 铆ndices est谩n en la matriz removedIndexes . Supongamos que tenemos una matriz de images de enteros que imita im谩genes, y una matriz de 铆ndices de estos enteros removedIndexes que deben eliminarse. Usaremos filter (_:) para resolver nuestro problema:

 var images = [6, 22, 8, 14, 16, 0, 7, 9] var removedIndexes = [2,5,0,6] var images1 = images .enumerated() .filter { !removedIndexes.contains($0.offset) } .map { $0.element } print (images1) // [22, 14, 16, 9] 

El m茅todo enumerated() devuelve una secuencia de tuplas que consta de 铆ndices de offset y valores de element matriz.Luego aplicamos un filtro filtera la secuencia resultante de tuplas, dejando solo aquellos cuyo 铆ndice $0.offsetno est谩 contenido en la matriz removedIndexes. El siguiente paso, seleccionamos el valor de la tupla $0.elementy obtenemos la matriz que necesitamos images1.

reduce (_:, _:)


El m茅todo reduce (_:, _:)tambi茅n est谩 disponible para la mayor铆a de los que est谩n disponibles map(_:)y los m茅todos filter (_:). El m茅todo reduce (_:, _:)"colapsa" la secuencia Sequenceen un solo valor de acumulaci贸n y tiene dos par谩metros. El primer par谩metro es el valor de acumulaci贸n inicial, y el segundo par谩metro es una funci贸n que combina el valor de acumulaci贸n con el elemento de secuencia Sequencepara obtener un nuevo valor de acumulaci贸n.

La funci贸n del par谩metro de entrada se aplica a cada elemento de la secuencia Sequence, uno tras otro, hasta que llega al final y crea el valor de acumulaci贸n final.

 let sum = Array (1...100).reduce(0, +) 

Este es un ejemplo trivial cl谩sico del uso de una funci贸n de orden superior reduce (_:, _:): contar la suma de los elementos de una matriz Array.

     1 0 1 0 +1 = 1 2 1 2 2 + 1 = 3 3 3 3 3 + 3 = 6 4 6 4 4 + 6 = 10 . . . . . . . . . . . . . . . . . . . 100 4950 100 4950 + 100 = 5050 

Usando la funci贸n, reduce (_:, _:)podemos calcular simplemente la suma de los n煤meros de Fibonacci que satisfacen una determinada condici贸n:

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] print(fibonacci.reduce(0,+)) // 3382 

Pero hay aplicaciones m谩s interesantes de una funci贸n de orden superior reduce (_:, _:).

Por ejemplo, podemos determinar de manera muy simple y concisa un par谩metro muy importante para UIScrollViewel tama帽o del 谩rea "desplazable" contentSizeen funci贸n de su tama帽o subviews:

 let scrollView = UIScrollView() scrollView.addSubview(UIView(frame: CGRect(x: 300.0, y: 0.0, width: 200, height: 300))) scrollView.addSubview(UIView(frame: CGRect(x: 100.0, y: 0.0, width: 300, height: 600))) scrollView.contentSize = scrollView.subviews .reduce(CGRect.zero,{$0.union($1.frame)}) .size // (500.0, 600.0) 

En esta demostraci贸n, el valor de acumulaci贸n es GCRect, y la operaci贸n de acumulaci贸n es la operaci贸n de combinar los unionrect谩ngulos que son framenuestros subviews.

A pesar de que una funci贸n de orden superior reduce (_:, _:)asume un car谩cter acumulativo, se puede usar en una perspectiva completamente diferente. Por ejemplo, para dividir una tupla en partes en una matriz de tuplas:

 // Separate Tuples let arr = [("one", 1), ("two", 2), ("three", 3), ("four", 4)] let (arr1, arr2) = arr.reduce(([], [])) { ($0.0 + [$1.0], $0.1 + [$1.1]) } print(arr1) // ["one", "two", "three", "four"] print(arr2) // [1, 2, 3, 4] 

Swift 4.2introdujo un nuevo tipo de funci贸n de orden superior reduce (into:, _:). El m茅todo reduce (into:, _:)es preferible en eficiencia en comparaci贸n con el m茅todo reduce (:, :)si COW (copy-on-write) , por ejemplo, Arrayo se usa como la estructura resultante Dictionary.

Puede usarse efectivamente para eliminar valores coincidentes en una matriz de enteros:

 // Remove duplicates let arrayInt = [1,1,2,6,6,7,2,9,7].reduce(into: []) { !$0.contains($1) ? $0.append($1) : () } // [1, 2, 6, 7, 9] 

... o al contar el n煤mero de elementos diferentes en una matriz:

 // Count equal elements in array let arrayIntCount = [1,1,2,2,6,6,7,2,9,7].reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1 } // [6: 2, 9: 1, 1: 2, 2: 3, 7: 2] 

flatMap (_:)


Antes de pasar a esta funci贸n de orden superior, veamos una demostraci贸n muy simple.

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Si ejecutamos este c贸digo para ejecutarlo Playground, entonces todo se ve bien, y el nuestro firstNumberes igual 42:



Pero, si no lo sabe, a Playgroundmenudo oculta el verdadero , en particular las constantes firstNumber. De hecho, la constante firstNumbertiene dos cosas Optional:



esto se debe a que map (Int.init)en la salida forma una matriz Optionalde valores TYPE [Int?], ya que no todas las l铆neas Stringpueden convertirse Inty el inicializador Int.intest谩 "cayendo" ( failable). Luego tomamos el primer elemento de la matriz formada usando la funci贸n firstpara la matriz Array, que tambi茅n forma la salidaOptional, ya que la matriz puede estar vac铆a y no podremos obtener el primer elemento de la matriz. Como resultado, tenemos un doble Optional, es decirInt?? .

Tenemos una estructura anidada Optionalen Optionalla que es realmente m谩s dif铆cil trabajar y que, naturalmente, no queremos tener. Para obtener el valor de esta estructura anidada, tenemos que "sumergirnos" en dos niveles. Adem谩s, cualquier transformaci贸n adicional puede profundizar Optionala煤n m谩s el nivel .

Obtener el valor del doble anidado es Optionalrealmente oneroso.

Tenemos 3 opciones y todas ellas requieren un conocimiento profundo del idioma Swift.

  • if let , ; 芦禄 芦禄 Optional , 鈥 芦禄 Optional :

  • if case let ( pattern match ) :



    ?? :

  • , switch :


Peor a煤n, tales problemas de anidaci贸n surgen en cualquier situaci贸n que involucre genericcontenedores generalizados ( ) para los cuales se define una operaci贸n map. Por ejemplo, para matrices Array.

Considere otro c贸digo de ejemplo. Supongamos que tenemos un texto de varias l铆neas multilineStringque queremos dividir en palabras escritas en min煤sculas (peque帽as):

 let multilineString = """  ,  ,   ;     , 鈥  ,   :  鈥   ,   .   ,   ,   .    .  ,        ,  芦 禄  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .map{$0.split(separator: " ")} 

Para obtener una matriz de palabras words, primero hacemos letras may煤sculas (grandes) min煤sculas (peque帽as) usando el m茅todo lowercased(), luego dividimos el texto en split(separatot: "\n")l铆neas usando el m茅todo y obtenemos una matriz de cadenas, y luego las usamos map {$0.split(separator: " ")}para separar cada l铆nea en palabras separadas.

Como resultado, obtenemos matrices anidadas:

 [["", ",", "", ","], ["", "", ";", "", "", "", "", ",", "鈥"], ["", ",", "", "", ":"], ["", "鈥", "", "", ",", "", "", "."], ["", "", ",", "", "", ","], ["", "", ".", "", ""], ["", ".", "", ",", ""], ["", "", "", ""], ["", "", ",", "", "芦", "禄"], ["", ".", "", ","], ["", ",", "", "", "!"]] 

... y wordstiene dos cosas Array:



nuevamente obtuvimos una estructura de datos "anidada", pero esta vez no Optional, pero Array. Si queremos continuar procesando las palabras recibidas words, por ejemplo, para encontrar el espectro de letras de este texto de varias l铆neas, primero tendremos que "enderezar" de alguna manera la matriz del doble Arrayy convertirla en una matriz 煤nica Array. Esto es similar a lo que hicimos con el doble Optionalpara una demostraci贸n al comienzo de esta secci贸n sobre flatMap:

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Afortunadamente, Swiftno tenemos que recurrir a construcciones sint谩cticas complejas. Swiftnos proporciona una soluci贸n preparada para matrices Arrayy Optional. Esta es una funci贸n de orden superior flatMap! Es muy similar a map, pero tiene una funcionalidad adicional asociada con el "enderezado" posterior de los "archivos adjuntos" que aparecen durante la ejecuci贸n map. Y es por eso que se llama flatMap, "endereza" ( flattens) el resultado map.

Intentemos aplicar flatMapa firstNumber:



Realmente obtuvimos la salida con un solo nivel Optional. Funciona

a煤n m谩s interesante flatMappara una matriz Array. En nuestra expresi贸n para, wordssimplemente reemplazamos mapconflatMap:



... y solo obtenemos una serie de palabras wordssin ning煤n tipo de "anidamiento":

 ["", ",", "", ",", "", "", ";", "", "", "", "", ",", "鈥", "", ",", "", "", ":", "", "鈥", "", "", ",", "", "", ".", "", "", ",", "", "", ",", "", "", ".", "", "", "", ".", "", ",", "", "", "", "", "", "", "", ",", "", "芦", "禄", "", ".", "", ",", "", ",", "", "", "!"] 

Ahora podemos continuar con el procesamiento que necesitamos de la matriz de palabras resultante words, pero tenga cuidado. Si lo aplicamos una vez m谩s flatMapa cada elemento de la matriz words, obtendremos, tal vez, un resultado inesperado, pero bastante comprensible.



Obtenemos una matriz 煤nica, no "anidada" de letras y s铆mbolos [Character]contenidos en nuestra frase de varias l铆neas:

 ["", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ";", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ...] 

El hecho es que la cadena Stringes una colecci贸n de Collectioncaracteres [Character]y, aplicando flatMapa cada palabra individual, una vez m谩s bajamos el nivel de "anidamiento" y llegamos a una serie de caracteres flattenCharacters.
Tal vez esto es exactamente lo que quieres, o tal vez no. Presta atenci贸n a esto.

Poniendo todo junto: resolviendo algunos problemas


TAREA 1


Podemos continuar el procesamiento que necesitamos de la matriz de palabras obtenida en la secci贸n anterior words, y calcular la frecuencia de aparici贸n de letras en nuestra frase de varias l铆neas. Primero, peguemos todas las palabras de la matriz wordsen una l铆nea grande y excluyamos todos los signos de puntuaci贸n, es decir, deje solo las letras:

 let wordsString = words.reduce ("",+).filter { "" .contains($0)} //  

Entonces, tenemos todas las letras que necesitamos. Ahora hagamos un diccionario de ellos, donde la clave keyes la letra, y el valor valuees la frecuencia de su aparici贸n en el texto.

Podemos hacer esto de dos maneras.
El primer m茅todo est谩 asociado con el uso de una nueva Swift 4.2variedad de una funci贸n de orden superior que ha aparecido en reduce (into:, _:). Este m茅todo es bastante adecuado para que organicemos un diccionario letterCountcon la frecuencia de aparici贸n de letras en nuestra frase de varias l铆neas:

 let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} print (letterCount) // ["": 1, "": 18, "": 2, "": 2, "": 5, "": 7, "": 17, "": 4, "": 23, ...] 

Como resultado, obtendremos un diccionario letterCount [Character : Int]en el que las claves keyson los caracteres encontrados en la frase en estudio, y como el valor valuees el n煤mero de estos caracteres.

El segundo m茅todo consiste en inicializar el diccionario usando la agrupaci贸n, que da el mismo resultado:

 let letterCountDictionary = Dictionary(grouping: wordsString ){ $0}.mapValues {$0.count} letterCount == letterCountDictionary // true 

Nos gustar铆a ordenar el diccionario letterCountalfab茅ticamente:

 let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] 

Pero no podemos ordenar el diccionario directamente Dictionary, ya que fundamentalmente no es una estructura de datos ordenada. Si aplicamos la funci贸n sorted (by:)al diccionario Dictionary, nos devolver谩 los elementos de la secuencia ordenados con el predicado dado en forma de un conjunto de tuplas con nombre, que mapconvertiremos en un conjunto de cadenas que [":17", ":5", ":18", ...]reflejan la frecuencia de aparici贸n de la letra correspondiente.

Vemos que esta vez sorted (by:)solo el operador " <" se pasa como un predicado a una funci贸n de orden superior . La funci贸n sorted (by:)espera una "funci贸n de comparaci贸n" como 煤nico argumento en la entrada. Se utiliza para comparar dos valores adyacentes y decidir si est谩n ordenados correctamente (en este caso, devuelvetrue) o no (devuelve false). Podemos dar a esta "funci贸n de comparaci贸n" funciones sorted (by:)en forma de cierre an贸nimo:

 sorted(by: {$0.key < $1.key} 

Y podemos darle el operador " <", que tiene la firma que necesitamos, como se hizo anteriormente. Esta tambi茅n es una funci贸n, y la clasificaci贸n por clave est谩 en progreso key.

Si queremos ordenar el diccionario por valores valuey descubrir qu茅 letras se encuentran con m谩s frecuencia en esta frase, entonces tendremos que usar el cierre para la funci贸n sorted (by:):

 let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

Si echamos un vistazo a la soluci贸n al problema de determinar el espectro de letras de una frase multil铆nea en su conjunto ...

 let multilineString = """  ,  ,   ;     , 鈥  ,   :  鈥   ,   .   ,   ,   .    .  ,        ,  芦 禄  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .flatMap{$0.split(separator: " ")} let wordsString = words.reduce ("",+).filter { "" .contains($0)} let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

... luego notaremos que en este fragmento de c贸digo b谩sicamente no hay variables (no var, solo let)todos los nombres de las funciones utilizadas reflejan ACCIONES (funciones) en cierta informaci贸n, sin preocuparse en absoluto de C脫MO se implementan estas acciones:

split- dividir,
map- transformar
flatMap- transformar con alineaci贸n (mediante la eliminaci贸n de un nivel de anidamiento),
filter- filtro,
sorted- clasificaci贸n,
reduce- para convertir los datos en una cierta estructura por medio de una operaci贸n espec铆fica

en este fragmento de cada l铆nea de c贸digo explica el nombre de la funci贸n que usamos si estamos en. rellenos transformaci贸n "pura" se utiliza mapsi realizamos se utiliza la conversi贸n del nivel de anidamientoflatMap, si queremos seleccionar solo ciertos datos, los usamos filter, etc. Todas estas funciones de "orden superior" est谩n dise帽adas y probadas Appleteniendo en cuenta la optimizaci贸n del rendimiento. Entonces, este c贸digo es muy confiable y conciso: no necesit谩bamos m谩s de 5 oraciones para resolver nuestro problema. Este es un ejemplo de programaci贸n funcional.

El 煤nico inconveniente de aplicar el enfoque funcional en esta demostraci贸n es que, en aras de la inmutabilidad, la capacidad de prueba y la legibilidad, perseguimos repetidamente nuestro texto a trav茅s de varias funciones de orden superior. En el caso de una gran cantidad de art铆culos de colecci贸n, el Collectionrendimiento puede caer en picado. Por ejemplo, si primero usamos filter(_:)y, y luego - first.
EnSwift 4 Se han agregado algunas opciones de funciones nuevas para mejorar el rendimiento, y aqu铆 hay algunos consejos para escribir c贸digo m谩s r谩pido.

1. Uso contains, NOfirst( where: ) != nil


La verificaci贸n de que un objeto est谩 en una colecci贸n Collectionse puede hacer de muchas maneras. El mejor rendimiento es proporcionado por la funci贸n contains.

C脫DIGO CORRECTO

 let numbers = [0, 1, 2, 3] numbers.contains(1) 

C脫DIGO INCORRECTO

 let numbers = [0, 1, 2, 3] numbers.filter { number in number == 1 }.isEmpty == false numbers.first(where: { number in number == 1 }) != nil 

2. Use validaci贸n isEmpty, NO una comparaci贸n countcon cero


Dado que para algunas colecciones el acceso a la propiedad countse realiza iterando sobre todos los elementos de la colecci贸n.

C脫DIGO CORRECTO

 let numbers = [] numbers.isEmpty 

C脫DIGO INCORRECTO

 let numbers = [] numbers.count == 0 

3. Verifique la cadena vac铆a StringconisEmpty


String Stringin Swiftes una colecci贸n de personajes [Character]. Esto significa que para cadenas tambi茅n es Stringmejor usar isEmpty.

C脫DIGO CORRECTO

 myString.isEmpty 

C脫DIGO INCORRECTO

 myString == "" myString.count == 0 

4. Obtener el primer elemento que satisface ciertas condiciones.


La iteraci贸n sobre toda la colecci贸n para obtener el primer objeto que satisfaga ciertas condiciones se puede realizar utilizando el m茅todo filterseguido por el m茅todo first, pero el m茅todo es el mejor en t茅rminos de velocidad first (where:). Este m茅todo deja de iterar sobre la colecci贸n tan pronto como cumple con la condici贸n necesaria. El m茅todo filtercontinuar谩 iterando sobre toda la colecci贸n, independientemente de si cumple con los elementos necesarios o no.

Obviamente, lo mismo es cierto para el m茅todo last (where:).

C脫DIGO CORRECTO

 let numbers = [3, 7, 4, -2, 9, -6, 10, 1] let firstNegative = numbers.first(where: { $0 < 0 }) 

C脫DIGO INCORRECTO

 let numbers = [0, 2, 4, 6] let allEven = numbers.filter { $0 % 2 != 0 }.isEmpty 

A veces, cuando la colecci贸n Collectiones muy grande y el rendimiento es cr铆tico para usted, vale la pena volver a comparar los enfoques imperativos y funcionales y elegir el que m谩s le convenga.

TAREA 2


Hay otro gran ejemplo de un uso muy conciso de una funci贸n de orden superior reduce (_:, _:)que he encontrado. Este es un juego SET .
Aqu铆 est谩n sus reglas b谩sicas. El nombre del juego SETproviene de la palabra inglesa "set" - "set". SET81 cartas participan en el juego , cada una con una imagen 煤nica:



cada carta tiene 4 atributos, que se enumeran a continuaci贸n:

Cantidad : cada carta tiene uno, dos o tres s铆mbolos.
Tipo de caracteres : 贸valos, rombos u ondas.
Color : los s铆mbolos pueden ser rojos, verdes o morados.
Relleno : los caracteres pueden estar vac铆os, sombreados o sombreados.

Prop贸sito del juego.SET: Entre las 12 cartas dispuestas sobre la mesa, debe encontrar SET(un conjunto) que consta de 3 cartas, en las que cada uno de los signos coincide completamente o difiere por completo en las 3 cartas. Todas las se帽ales deben cumplir completamente con esta regla.

Por ejemplo, el n煤mero de caracteres en las 3 cartas debe ser igual o diferente, el color en las 3 cartas debe ser igual o diferente, y as铆 sucesivamente ...

En este ejemplo, solo nos interesar谩 el Modelo de mapa SET struct SetCardy el algoritmo para determinar SETpor 3er mapas isSet( cards:[SetCard]):

 struct SetCard: Equatable { let number: Variant // number - 1, 2, 3 let color: Variant // color - 1, 2, 3 (, , , ) let shape: Variant // symbol - 1, 2, 3 (, , , ) let fill: Variant // fill - 1, 2, 3 (, , , ) enum Variant: Int, CaseIterable { case v1 = 1 case v2 case v3 } static func isSet(cards: [SetCard]) -> Bool { guard cards.count == 3 else {return false} let sums = [ cards.reduce(0, { $0 + $1.number.rawValue }), cards.reduce(0, { $0 + $1.color.rawValue }), cards.reduce(0, { $0 + $1.shape.rawValue }), cards.reduce(0, { $0 + $1.fill.rawValue }) ] return sums.reduce(true, { $0 && ($1 % 3 == 0) }) } } 

Los modelos cada caracter铆stica - n煤mero number , tipo de s铆mbolo shape , de color color y de llenado fill - presentados lista Variantque tiene tres valores posibles var1, var2y var3que corresponde a la 3陋 enteros rawValue- 1,2,3. De esta forma, rawValuees f谩cil de operar. Si tomamos una indicaci贸n, por ejemplo color, a continuaci贸n, a帽adir todo rawValuede colors3 cartas, nos encontramos con que si colorslos 3 cartas son iguales, la cantidad ser谩 igual a 3, 6o 9, si son diferentes, entonces la cantidad ser谩 iguales 6. En cualquiera de estos casos, tenemos la multiplicidad de la tercera suma rawValueparacolorsLas 3 cartas. Sabemos que este es un requisito previo para lo que componen 3 cartas SET. Para que 3 cartas sean realmente SETnecesarias, para todos los signos SetCard(Cantidad number, Tipo de s铆mbolo shape, Color colory Relleno fill) su suma debe rawValueser un m煤ltiplo de la 3ra.

Por lo tanto, en el staticm茅todo, isSet( cards:[SetCard])primero calculamos la matriz sumsde las sumas rawValuepara todos los 3 mapas para todos mapa 4 rendimiento utilizando funci贸n de orden superior reducecon un valor inicial igual a 0, y la acumulaci贸n de funciones {$0 + $1.number.rawValue}, {$0 + $1.color.rawValue}, {$0 + $1.shape.rawValue}, { {$0 + $1.fill.rawValue}. Cada elemento de la matriz sumsdebe ser un m煤ltiplo de 3er, y nuevamente usamos la funci贸nreduce, pero esta vez con un valor inicial igual truey que acumula la funci贸n l贸gica " AND" {$0 && ($1 % 3) == 0}. En Swift 5, para probar la multiplicidad de un n煤mero por otro, se introduce una funci贸n en isMultiply(of:)lugar del operador %restante. Tambi茅n mejorar谩 la legibilidad del c贸digo: { $0 && ($1.isMultiply(of:3) }.

Este c贸digo fant谩sticamente corto para descubrir si 3 SetCardcartas son las SETi-茅simas se obtiene gracias al enfoque " funcional ", y podemos asegurarnos de que funcione en Playground:



C贸mo SETconstruir la interfaz de usuario ( UI) en este Modelo de juego aqu铆 , aqu铆 y aqu铆 .

Caracter铆sticas puras y efectos secundarios.


Una funci贸n pura cumple dos condiciones. Siempre devuelve el mismo resultado con los mismos par谩metros de entrada. Y el c谩lculo del resultado no causa efectos secundarios asociados con la salida de datos externos (por ejemplo, al disco) o con el pr茅stamo de datos fuente del exterior (por ejemplo, el tiempo). Esto le permite optimizar significativamente el c贸digo.

Este tema se Swiftexpone perfectamente en point.free en los primeros episodios de " Funciones " y " Efectos secundarios " , que se traducen al ruso y se presentan como " Funciones " y "Efectos secundarios" .

Composici贸n de la funci贸n


En un sentido matem谩tico, esto significa aplicar una funci贸n al resultado de otra funci贸n. En una Swiftfunci贸n, pueden devolver un valor que puede usar como entrada para otra funci贸n. Esta es una pr谩ctica de programaci贸n com煤n.

Imagine que tenemos una matriz de enteros y queremos obtener una matriz de cuadrados de n煤meros pares 煤nicos en la salida. Por lo general, implementamos esto de la siguiente manera:

 var integerArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] func unique(_ array: [Int]) -> [Int] { return array.reduce(into: [], { (results, element) in if !results.contains(element) { results.append(element) } }) } func even(_ array: [Int]) -> [Int] { return array.filter{ $0%2 == 0} } func square(_ array: [Int]) -> [Int] { return array.map{ $0*$0 } } var array = square(even(unique(integerArray))) // it returns [4, 16, 36, 64] 

Este c贸digo nos da el resultado correcto, pero ve que la legibilidad de la 煤ltima l铆nea de c贸digo no es tan f谩cil. La secuencia de funciones (de derecha a izquierda) es la opuesta a la que estamos acostumbrados (de izquierda a derecha) y nos gustar铆a ver aqu铆. Necesitamos dirigir nuestra l贸gica primero a la parte m谩s interna de las incrustaciones m煤ltiples: a una matriz inegerArray, luego a una funci贸n externa a esta matriz unique, luego subimos un nivel m谩s: una funci贸n eveny, finalmente, una funci贸n en conclusi贸n square.

Y aqu铆 la "composici贸n" de funciones >>>y operadores nos ayuda |>, lo que nos permite escribir el c贸digo de una manera muy conveniente, representando el procesamiento de la matriz original integerArraycomo un "transportador" de funciones:

 var array1 = integerArray |> unique >>> even >>> square 

Casi todos los lenguajes de programaci贸n tales como funcional especializada F#, Elixiry Elmutilizar estos operadores para las funciones de "composici贸n".

No Swifthay operadores integrados de la "composici贸n" de funciones >>>y |>, pero podemos obtenerlos f谩cilmente con la ayuda de Genericsclosures ( closure) y el infixoperador:

 precedencegroup ForwardComposition{ associativity: left higherThan: ForwardApplication } infix operator >>> : ForwardComposition func >>> <A, B, C>(left: @escaping (A) -> B, right: @escaping (B) -> C) -> (A) -> C { return { right(left($0)) } } precedencegroup ForwardApplication { associativity: left } infix operator |> : ForwardApplication func |> <A, B>(a: A, f: (A) -> B) -> B { return f(a) } 

A pesar de los costos adicionales, en algunos casos esto puede aumentar significativamente el rendimiento, la legibilidad y la capacidad de prueba de su c贸digo. Por ejemplo, cuando dentro de mapusted coloca una cadena completa de funciones utilizando el operador "composici贸n" en >>>lugar de perseguir una matriz a trav茅s de numerosos map:

 var integerArray1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] let b = integerArray1.map( { $0 + 1 } >>> { $0 * 3 } >>> String.init) print (b) // ["6", "9", "12", "15", "18", "21", "24", "27", "30", "6", "15", "18"] 

Pero no siempre un enfoque funcional da un efecto positivo.

Al principio, cuando apareci贸 Swiften 2014, todos se apresuraron a escribir bibliotecas con operadores para la "composici贸n" de funciones y resolver una tarea dif铆cil para ese momento, como analizar JSONusando operadores de programaci贸n funcional en lugar de usar construcciones infinitamente anidadas if let. Yo mismo traduje el art铆culo sobre el an谩lisis funcional JSON que me deleit贸 con su elegante soluci贸n y era fan谩tico de la biblioteca Argo .

Pero los desarrolladores Swifttomaron un camino completamente diferente y propusieron, sobre la base de la tecnolog铆a orientada al protocolo, una forma mucho m谩s concisa de escribir c贸digo. Para "entregar" los JSONdatos directamente aLo suficiente como para hacer esto Codable, que implementa autom谩ticamente este protocolo, si el modelo se compone de las conocidas Swiftestructuras de datos: String, Int, URL, Array, Dictionary, etc.

 struct Blog: Codable { let id: Int let name: String let url: URL } 

Tener JSONdatos de ese famoso art铆culo ...
 [ { "id" : 73, "name" : "Bloxus test", "url" : "http://remote.bloxus.com/" }, { "id" : 74, "name" : "Manila Test", "url" : "http://flickrtest1.userland.com/" } ] 

... en este momento solo necesita una l铆nea de c贸digo para obtener una variedad de blogs blogs:

 let blogs = Bundle.main.path(forResource: "blogs", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Blog].self, from: $0) } print ("\(blogs!)") // [id: 73 name: Bloxus test url: http://remote.bloxus.com/, // id: 74 name: Manila Test url: http://flickrtest1.userland.com/] 

Todo el mundo se ha olvidado de usar los operadores de la "composici贸n" de funciones para analizar JSON, si hay otra manera m谩s f谩cil y comprensible de hacerlo mediante protocolos.

Si todo es tan f谩cil, entonces podemos "subir" JSONdatos a modelos m谩s complejos. Supongamos que tenemos un archivo de JSONdatos que tiene un nombre user.jsony se encuentra en nuestro directorio Resources.. Contiene datos sobre un usuario:

 { "email": "blob@pointfree.co", "id": 42, "name": "Blob" } 

Y tenemos un Codable usuario Usercon un inicializador de los datos json:

 struct User: Codable { let email: String let id: Int let name: String init?(json: Data) { if let newValue = try? JSONDecoder().decode(User.self, from: json) { self = newValue } else { return nil } } } 

Podemos obtener f谩cilmente un nuevo usuario newUsercon un c贸digo funcional a煤n m谩s simple:

 let newUser = Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) } 

Obviamente, newUserhabr谩 un TIPO Optional, es decir User?:



supongamos que en nuestro directorio Resourceshay otro archivo con un nombre invoices.jsony contiene datos sobre las facturas de este usuario.

 [ { "amountPaid": 1000, "amountDue": 0, "closed": true, "id": 1 }, { "amountPaid": 500, "amountDue": 500, "closed": false, "id": 2 } ] 

Podemos cargar estos datos exactamente como lo hicimos con User. Definamos la estructura como un modelo de factura struct Invoice...

 struct Invoice: Codable { let amountDue: Int let amountPaid: Int let closed: Bool let id: Int } 

... y decodifica la JSONmatriz de facturas presentada anteriormente invoices, cambiando solo la ruta del archivo y la l贸gica de decodificaci贸n decode:

 let invoices = Bundle.main.path(forResource: "invoices", ofType: "json") .map( URL.init(fileURLWithPath:) ) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } 

invoicesser谩 [Invoice]?:



Ahora nos gustar铆a conectar al usuario usercon sus facturas invoices, si no son iguales nil, y guardar, por ejemplo, en la estructura del sobre UserEnvelopeque se env铆a al usuario junto con sus facturas:

 struct UserEnvelope { let user: User let invoices: [Invoice] } 

En lugar de actuar dos veces if let...

 if let newUser = newUser, let invoices = invoices { } 

... escribamos un an谩logo funcional del doble if letcomo una Genericfunci贸n auxiliar zipque convierte dos Optionalvalores en una Optionaltupla:

 func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? { if let a = a, let b = b { return (a, b) } return nil } 

隆Ahora no tenemos ninguna raz贸n para asignar algo a las variables newUsery invoices, simplemente construimos todo en nuestra nueva funci贸n zip, usamos el inicializador UserEnvelope.inity todo funcionar谩!

 let userEnv = zip( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ).flatMap (UserEnvelope.init) print ("\(userEnv!)") // UserEnvelope(user: id: 42 name: Blob , // invoices: [id: 1 amountDue: 0 amountPaid: 1000 closed: true, // id: 2 amountDue: 500 amountPaid: 500 closed: false]) 

En una sola expresi贸n, se empaqueta un algoritmo completo para entregar JSONdatos a uno complejo en forma de estructura struct UserEnvelope.

  • zip , , . user , JSON , invoices , JSON . .
  • map , , 芦禄 .
  • flatMap , , , .

Operaciones zip, mapy flatMaprepresentan un tipo de lenguaje espec铆fico de dominio (DSL) para la conversi贸n de datos.

Podemos desarrollar a煤n m谩s esta demostraci贸n para representar la lectura as铆ncrona del contenido de un archivo como una funci贸n especial que puede ver en pointfree.co .

No soy un fan谩tico de la programaci贸n funcional en todas partes y en todo, pero me parece aconsejable su uso moderado.

Conclusi贸n


Di ejemplos de varios programaci贸n funcional dispone de Swft 芦fuera de la caja", basado en el uso de funciones de orden superior map, flatMap, reduce, filtery el otro para las secuencias Sequence, Optionaly Result. Pueden ser los "caballos de batalla" de la creaci贸n de c贸digo, ,especialmente si las estructuras de valor structy las enumeraciones est谩n involucradas all铆 enum. Un desarrollador de iOSaplicaciones debe poseer esta herramienta.

Todas las demos compiladas Playgroundse pueden encontrar en Github . Si tiene problemas con el lanzamiento Playground, puede ver este art铆culo:

C贸mo deshacerse de los errores de "congelaci贸n" de Xcode Playground con los mensajes "Launching Simulator" y "Running Playground".

Referencias

Functional Programming in Swift: An Introduction.
An Introduction to Functional Programming in Swift.
The Many Faces of Flat-Map: Part 3
Inside the Standard Library: Sequence.map()
Practical functional programming in Swift

Source: https://habr.com/ru/post/440722/


All Articles