Um pouco de prática da programação funcional no Swift para iniciantes



Eu gostaria de apresentar o conceito de Programação Funcional para iniciantes da maneira mais simples, destacando algumas de suas vantagens das muitas outras que realmente tornarão o código mais legível e expressivo. Peguei algumas demos interessantes para você que estão no Playground no Github .

Programação Funcional: Definição


Primeiro de tudo, a Programação Funcional não é uma linguagem ou sintaxe, mas provavelmente uma maneira de resolver problemas dividindo processos complexos em processos mais simples e sua composição subseqüente. Como o nome indica, “ Programação Funcional, a unidade de composição para essa abordagem é uma função ; e o objetivo dessa função é evitar alterar estados ou valores fora de seu scope) .

No Swift World, existem todas as condições para isso, porque as funções aqui são participantes tão plenos do processo de programação quanto os objetos, e o problema da mutation é resolvido no nível do conceito de value TYPES (estruturas e enum enuméricas) que ajudam a gerenciar a mutabilidade ( mutation ) e comunique claramente como e quando isso pode acontecer.

No entanto, o Swift não Swift no sentido pleno da linguagem de programação funcional , não o força a programar com funcionalidade , embora reconheça as vantagens das abordagens funcionais e encontre maneiras de incorporá-las.

Neste artigo, focaremos o uso dos elementos internos da Programação Funcional no Swift (isto é, "pronto para uso") e entender como você pode usá-los confortavelmente em seu aplicativo.

Abordagens Imperativas e Funcionais: Comparação


Para avaliar a Abordagem Funcional , vamos comparar as soluções para um problema simples de duas maneiras diferentes. A primeira solução é " Imperativo " , na qual o código altera o estado dentro do 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 os valores dentro da matriz mutável denominada numbers e, em seguida, imprimimos no console. Observando este código, tente responder às seguintes perguntas que discutiremos em um futuro próximo:

  1. O que você está tentando alcançar com seu código?
  2. O que acontece se outro thread tentar acessar a matriz de numbers enquanto seu código estiver em execução?
  3. O que acontece se você deseja ter acesso aos valores originais na matriz de numbers ?
  4. Quão confiável esse código pode ser testado?

Agora vamos ver uma abordagem alternativa " Funcional ":

 //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] 

Neste trecho de código, obtemos o mesmo resultado no console, abordando a solução do problema de uma maneira completamente diferente. Observe que desta vez nossa matriz de numbers é imutável graças à palavra-chave let . timesTen() o processo de multiplicação de números da matriz de numbers para o método timesTen() , localizado na extensão de extension Array . Ainda usamos um loop for e modificamos uma variável chamada output , mas o scope dessa variável é limitado apenas por esse método. Da mesma forma, nosso argumento de entrada self é passado para o método timesTen() por valor ( by value ), tendo o mesmo escopo que a variável de output . O método timesTen() é chamado e podemos imprimir no console a matriz de numbers original e o resultado da matriz de result .
Vamos voltar às nossas 4 perguntas.

1. O que você está tentando alcançar com seu código?

Em nosso exemplo, realizamos uma tarefa muito simples multiplicando os números na matriz de numbers por 10 .

Com uma abordagem imperativa , para obter uma saída, você precisa pensar como um computador, seguindo as instruções no loop for . Nesse caso, o código mostra você consegue o resultado. Com a Abordagem Funcional , “ ” é “ timesTen() ” no método timesTen() . Desde que esse método tenha sido implementado em outro lugar, você poderá realmente ver apenas a expressão numbers.timesTen() . Esse código mostra claramente o alcançado por esse código, e não a tarefa é resolvida. Isso é chamado de Programação Declarativa e é fácil adivinhar por que essa abordagem é atraente. A abordagem imperativa faz o desenvolvedor entender código funciona para determinar o ele deve fazer. A abordagem funcional comparada à abordagem Imperative é muito mais "expressiva" e oferece ao desenvolvedor uma oportunidade de luxo de simplesmente assumir que o método faz o que ele afirma fazer! (Obviamente, essa suposição se aplica apenas ao código pré-verificado).

2. O que acontece se outro thread tentar acessar a matriz de numbers enquanto seu código estiver em execução?

Os exemplos apresentados acima existem em um espaço completamente isolado, embora em um ambiente multiencadeado complexo, é bem possível que dois threads tentem acessar os mesmos recursos simultaneamente. No caso da abordagem Imperative , é fácil ver que quando outro thread tiver acesso à matriz de numbers no processo de uso, o resultado será ditado pela ordem em que os threads acessam a matriz de numbers . Essa situação é chamada de race condition e pode levar a comportamento imprevisível e até instabilidade e travamento do aplicativo.

Em comparação, a Abordagem Funcional não tem "efeitos colaterais". Em outras palavras, a saída do método de output não altera nenhum valor armazenado em nosso sistema e é determinada apenas pela entrada. Nesse caso, qualquer thread ( threads ) que tenha acesso à matriz numbers receberá SEMPRE os mesmos valores e seu comportamento será estável e previsível.

3. O que acontece se você deseja ter acesso aos valores originais armazenados na matriz de numbers ?

Esta é uma continuação da nossa discussão sobre "efeitos colaterais". Obviamente, as mudanças de estado não são rastreadas. Portanto, com a abordagem Imperative , perdemos o estado inicial de nossa matriz de numbers durante o processo de conversão. Nossa solução, baseada na Abordagem Funcional , salva a matriz de numbers originais e gera uma nova matriz de result com as propriedades desejadas na saída. Ele deixa a matriz de numbers original intacta e adequada para processamento futuro.

4. Quão confiável esse código pode ser testado?

Como a abordagem Funcional destrói todos os "efeitos colaterais", a funcionalidade testada está completamente dentro do método. A entrada desse método NUNCA sofrerá alterações, portanto, você pode testar várias vezes usando o ciclo quantas vezes quiser e sempre obterá o mesmo resultado. Nesse caso, o teste é muito fácil. Em comparação, testar a solução Imperative em um loop alterará o início da entrada e você obterá resultados completamente diferentes após cada iteração.

Resumo dos Benefícios


Como vimos em um exemplo muito simples, a Abordagem Funcional é uma coisa interessante se você estiver lidando com um Modelo de Dados porque:

  • É declarativo
  • Corrige problemas relacionados a threads, como race condition e deadlocks
  • Ele deixa o estado inalterado, que pode ser usado para transformações subsequentes.
  • É fácil de testar.

Vamos um pouco mais longe no aprendizado da Programação Funcional no Swift . Ele assume que os principais "atores" são funções e devem ser principalmente objetos da primeira classe .

Funções de primeira classe e funções de ordem superior


Para que uma função seja de primeira classe, ela deve ter a capacidade de ser declarada como uma variável. Isso permite gerenciar a função como um TIPO de dados normal e, ao mesmo tempo, executá-la. Felizmente, em Swift funções são objetos da primeira classe, ou seja, são suportadas passando-as como argumentos para outras funções, retornando-as como resultado de outras funções, atribuindo-as a variáveis ​​ou armazenando-as em estruturas de dados.

Por isso, temos outras funções no Swift - funções de ordem superior que são definidas como funções que assumem outra função como argumento ou retornam uma função. Existem muitos deles: map , filter , reduce , forEach , flatMap , compactMap , sorted , etc. Os exemplos mais comuns de funções de ordem superior são map , filter e reduce . Eles não são globais, estão todos "apegados" a certos TIPOS. Eles funcionam em todos os TIPOS de Sequence , incluindo a Collection , que é representada por estruturas de dados Swift , como uma Array , um Dictionary e um Set . No Swift 5 , funções de ordem superior também funcionam com um TYPE - Result completamente novo.

map(_:)


No map(_:) Swift map(_:) assume uma função como parâmetro e converte os valores de um determinado acordo com esta função. Por exemplo, aplicando map(_:) a uma matriz de valores de Array , aplicamos uma função de parâmetro a cada elemento da matriz original e obtemos uma matriz de Array , mas também os 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] 

No código acima, criamos a função timesTen (_:Int) , que pega um valor inteiro Int e retorna o valor inteiro Int multiplicado por 10 , e o usamos como parâmetro de entrada para nossa função de map(_:) ordem superior map(_:) , aplicando-a à nossa matriz numbers . Temos o resultado que precisamos na matriz de result .

O nome da função de parâmetro timesTen para funções de ordem superior como map(_:) não importa, o parâmetro de entrada e o valor de retorno são importantes, ou seja, a assinatura (Int) -> Int parâmetro de entrada de função. Portanto, podemos usar funções anônimas no map(_:) - closures - de qualquer forma, incluindo aquelas com nomes abreviados de argumentos $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] 

Se olharmos para a função map(_ :) para uma Array , ela pode ser assim:

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

Este é um código imperativo que nos é familiar, mas não é mais um problema de desenvolvedor, é um problema da Apple , um problema do Swift . A implementação da função de map(_:) ordem superior map(_:) é otimizada pela Apple em termos de desempenho, e nós, os desenvolvedores, temos a funcionalidade do map(_:) garantida, para que possamos expressar corretamente corretamente com a função de argumento de transform queremos, sem se preocupar com será implementado. Como resultado, obtemos um código perfeitamente legível na forma de uma única linha, que funcionará melhor e mais rapidamente.

 //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)] 

O retornado pela função de parâmetro pode não coincidir com o elementos na coleção original.

No código acima, temos possíveis números inteiros possibleNumbers , representados como seqüências de caracteres, e queremos convertê-los em números inteiros de Int , usando o inicializador disponível Int(_ :String) representado pelo fechamento { str in Int(str) } . Fazemos isso usando map(_:) e obtemos uma matriz mapped de Optional como a saída:



foi possível converter elementos da nossa matriz possibleNumbers em números inteiros, como resultado, uma parte recebeu o valor nil , indicando a impossibilidade de converter a String em um inteiro Int e a outra parte transformada em Optionals , que possuem valores:

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

compactMap(_ :)


Se a função de parâmetro passada para a função de ordem superior tiver um valor Optional na saída, pode ser mais útil usar outra função de ordem superior, com significado semelhante - compactMap(_ :) , que faz a mesma coisa que map(_:) , mas adicionalmente "expande" os valores recebidos na saída Optional e remove valores nil da coleção.



Nesse caso, obtemos uma matriz de compactMapped TYPE [Int] , mas possivelmente menor:

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



Sempre que você usar o init?() Como a função de transformação, será necessário usar o 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] 

Devo dizer que existem razões mais do que suficientes para usar a função de ordem superior compactMap(_ :) . Swift “ama” Valores Optional , eles podem ser obtidos não apenas usando o init?()failableinit?() , mas também usando o as? "Fundição":

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

... e a try? ao processar erros gerados por alguns métodos. Devo dizer que a Apple preocupada que o uso de try? muitas vezes leva a dobrar Optional e no Swift 5 agora deixa apenas um nível Optional depois de aplicar a try? .

Há mais uma função semelhante no nome do flatMap(_ :) ordem superior flatMap(_ :) , sobre o qual um pouco menor.

Às vezes, para usar o map(_:) funções de ordem superior map(_:) , é útil usar o método zip (_:, _:) para criar uma sequência de pares a partir de duas seqüências diferentes.

Suponha que tenhamos uma view na qual vários pontos são representados, conectados entre si e formando uma linha quebrada:



Precisamos construir outra linha quebrada conectando os pontos médios dos segmentos da linha quebrada original:



Para calcular o ponto médio de um segmento, precisamos ter as coordenadas de dois pontos: o atual e o próximo. Para fazer isso, podemos criar uma sequência que consiste em pares de pontos - o atual e o próximo - usando o método zip (_:, _:) points.dropFirst() zip (_:, _:) , no qual usaremos a matriz de pontos iniciais e a matriz dos seguintes 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 )} 

Tendo essa sequência, calculamos muito facilmente os pontos médios usando o map(_:) funções de ordem superior map(_:) e os exibimos no gráfico.

filter (_:)


No Swift , o filter (_:) função de ordem superior filter (_:) está disponível para a maioria dos quais a função de map(_:) está disponível. Você pode filtrar qualquer Sequence sequência com o filter (_:) , isso é óbvio! O método filter (_:) assume outra função como parâmetro, que é uma condição para cada elemento da sequência e, se a condição for satisfeita, o elemento será incluído no resultado e, se não, não será incluído. Essa "outra função" pega um valor único - um elemento da sequência Sequence - e retorna um Bool , o chamado predicado.

Por exemplo, para matrizes de Array , o filter (_:) função de ordem superior filter (_:) aplica a função de predicado e retorna outra matriz que consiste apenas nos elementos da matriz original para os quais a função de predicado de entrada retorna 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] 

Aqui, o filter (_:) função de ordem superior filter (_:) pega cada elemento da matriz de numbers (representado por $0 ) e verifica se esse elemento é um número par. Se esse for um número par, os elementos da matriz de numbers caem na nova matriz filted , caso contrário não. Nós, de forma declarativa, informamos ao programa o que queremos obter, em vez de nos preocuparmos com devemos fazê-lo.

Vou dar outro exemplo de como usar o filter (_:) função de ordem superior filter (_:) para obter apenas os 20 primeiros números de Fibonacci com 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] 

Obtemos uma sequência de tuplas que consiste em dois elementos da sequência de Fibonacci: a n-ésima e (n + 1) -a:

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

Para processamento adicional, limitamos o número de elementos aos vigésimos primeiros elementos usando o prefix (20) e 0 o 0 elemento da tupla formada usando o map {$0.0 } , que corresponderá à sequência de Fibonacci iniciando com 0 :

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

Poderíamos pegar o 1 elemento da tupla formada usando o map {$0.1 } , que corresponderia à sequência de Fibonacci começando com 1 :

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

Obtemos os elementos necessários com a ajuda do filter {$0 % 2 == 0 && $0 < 4000} função de ordem superior filter {$0 % 2 == 0 && $0 < 4000} , que retorna uma matriz de elementos de sequência que satisfazem o predicado especificado. No nosso caso, será uma matriz de números inteiros [Int] :

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

Há outro exemplo útil de uso de filter (_:) para uma Collection .

Me deparei com um problema real quando você tem uma série de imagens que são exibidas usando o CollectionView e pode usar a tecnologia Drag & Drop para coletar um "pacote" inteiro de imagens e movê-las para qualquer lugar, incluindo "dumping" para " lixeira ".



Nesse caso, a matriz de índices removedIndexes despejadas na "lixeira" é corrigida e você precisa criar uma nova matriz de imagens, excluindo aquelas cujos índices estão na matriz removedIndexes . Suponha que tenhamos uma matriz de images inteiras que simula imagens e uma matriz de índices desses inteiros removedIndexes que precisam ser removidos. Usaremos o filter (_:) para resolver nosso 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] 

O método enumerated() retorna uma sequência de tuplas que consiste em índices de offset e valores de element matriz.Em seguida, aplicamos um filtro filterà sequência resultante de tuplas, deixando apenas aquelas cujo índice $0.offsetnão está contido na matriz removedIndexes. A próxima etapa, selecionamos o valor da tupla $0.elemente obtemos a matriz que precisamos images1.

reduce (_:, _:)


O método reduce (_:, _:)também está disponível para a maioria dos que estão disponíveis map(_:)e métodos filter (_:). O método reduce (_:, _:)"recolhe" a sequência Sequencepara um único valor acumulado e possui dois parâmetros. O primeiro parâmetro é o valor inicial da acumulação e o segundo parâmetro é uma função que combina o valor acumulado com o elemento de sequência Sequencepara obter um novo valor acumulado.

A função do parâmetro de entrada é aplicada a cada elemento da sequência Sequence, um após o outro, até chegar ao final e criar o valor acumulado final.

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

Este é um exemplo trivial clássico do uso de uma função de ordem superior reduce (_:, _:)- contando a soma dos elementos de uma 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 a função, reduce (_:, _:)podemos calcular muito facilmente a soma dos números de Fibonacci que atendem a uma determinada condição:

 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 

Mas existem aplicações mais interessantes de uma função de ordem superior reduce (_:, _:).

Por exemplo, podemos determinar de maneira muito simples e concisa um parâmetro muito importante para UIScrollView- o tamanho da área "rolável" contentSize- com base em seu tamanho 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) 

Nesta demonstração, o valor acumulado é GCRecte a operação acumulativa é a operação de combinar os unionretângulos que são framenossos subviews.

Apesar de uma função de ordem superior reduce (_:, _:)assumir um caráter acumulativo, ela pode ser usada em uma perspectiva completamente diferente. Por exemplo, para dividir uma tupla em partes em uma 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.2introduziu um novo tipo de função de ordem superior reduce (into:, _:). O método reduce (into:, _:)é preferível em eficiência em comparação com o método reduce (:, :)se COW (copy-on-write) , por exemplo, Arrayou for usado como a estrutura resultante Dictionary.

Ele pode ser usado efetivamente para remover valores correspondentes em uma matriz de números inteiros:

 // 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] 

... ou ao contar o número de elementos diferentes em uma 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 prosseguir para esta função de ordem superior, vejamos uma demonstração muito simples.

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

Se executarmos esse código para executar Playground, tudo parecerá bom e o nosso firstNumberserá igual 42:



mas, se você não souber, Playgroundmuitas vezes oculta o verdadeiro , em particular as constantes firstNumber. De fato, a constante firstNumbertem duas coisas Optional:



Isso ocorre porque, map (Int.init)na saída, forma uma matriz Optionalde valores TYPE [Int?], pois nem todas as linhas Stringpodem ser convertidas Inte o inicializador Int.intestá "caindo" ( failable). Em seguida, pegamos o primeiro elemento da matriz formada usando a função firstda matriz Array, que também forma a saídaOptional, pois a matriz pode estar vazia e não conseguiremos obter o primeiro elemento da matriz. Como resultado, temos um duplo Optional, ou seja Int??.

Temos uma estrutura aninhada Optionalna Optionalqual é realmente mais difícil trabalhar e que naturalmente não queremos ter. Para obter o valor dessa estrutura aninhada, precisamos “mergulhar” em dois níveis. Além disso, quaisquer transformações adicionais podem aprofundar Optionalainda mais o nível .

Obter o valor do aninhado duplo é Optionalrealmente oneroso.

Temos três opções e todas elas exigem um conhecimento profundo do idioma Swift.

  • if let , ; «» «» Optional , — «» Optional :

  • if case let ( pattern match ) :



    ?? :

  • , switch :


, «» , ( generic ) , map . , Array .

. , multilineString , , () :

 let multilineString = """  ,  ,   ;     , —  ,   :  —   ,   .   ,   ,   .    .  ,        ,  « »  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .map{$0.split(separator: " ")} 

, words , ( ) () lowercased() , c split(separatot: "\n") , map {$0.split(separator: " ")} .

:

 [["", ",", "", ","], ["", "", ";", "", "", "", "", ",", "—"], ["", ",", "", "", ":"], ["", "—", "", "", ",", "", "", "."], ["", "", ",", "", "", ","], ["", "", ".", "", ""], ["", ".", "", ",", ""], ["", "", "", ""], ["", "", ",", "", "«", "»"], ["", ".", "", ","], ["", ",", "", "", "!"]] 

... e wordstem um duplo Array:



novamente temos uma estrutura de dados "aninhada", mas desta vez não temos Optional, mas Array. Se quisermos continuar processando as palavras recebidas words, por exemplo, para encontrar o espectro de letras desse texto com várias linhas, primeiro teremos que "endireitar" de maneira alguma a matriz da dupla Arraye transformá-la em uma única matriz Array. Isso é semelhante ao que fizemos com o dobro Optionalde uma demonstração no início desta seção flatMap:

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

Swift . Swift Array Optional . flatMap ! map , , «» «», map . flatMap , «» ( flattens ) map .

flatMap firstNumber :



c Optional .

flatMap Array . words map flatMap:



... e acabamos de obter uma matriz de palavras wordssem nenhum "aninhamento":

 ["", ",", "", ",", "", "", ";", "", "", "", "", ",", "—", "", ",", "", "", ":", "", "—", "", "", ",", "", "", ".", "", "", ",", "", "", ",", "", "", ".", "", "", "", ".", "", ",", "", "", "", "", "", "", "", ",", "", "«", "»", "", ".", "", ",", "", ",", "", "", "!"] 

Agora podemos continuar o processamento que precisamos da matriz de palavras resultante words, mas tenha cuidado. Se o aplicarmos novamente flatMapa cada elemento da matriz words, obteremos, talvez, um resultado inesperado, mas bastante compreensível.



Temos uma única matriz, não "aninhada", de letras e símbolos [Character]contidos em nossa frase de várias linhas:

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

O fato é que a string Stringé uma coleção de Collectioncaracteres [Character]e, aplicando-se flatMapa cada palavra individual, abaixamos novamente o nível de "aninhamento" e chegamos a uma variedade de caracteres flattenCharacters.
Talvez seja exatamente isso que você deseja ou talvez não. Preste atenção nisso.

Juntando tudo: resolvendo alguns problemas


TAREFA 1


Podemos continuar o processamento que precisamos da matriz de palavras obtida na seção anterior wordse calcular a frequência de ocorrência de letras em nossa frase de várias linhas. Para começar, vamos "colar" todas as palavras da matriz wordsem uma linha grande e excluir todos os sinais de pontuação, ou seja, deixe apenas as letras:

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

Então, nós temos todas as letras que precisamos. Agora vamos fazer um dicionário deles, onde a chave keyé a letra e o valor valueé a frequência de sua ocorrência no texto.

Podemos fazer isso de duas maneiras.
O primeiro método está associado ao uso de uma nova Swift 4.2variedade de uma função de ordem superior que apareceu reduce (into:, _:). Este método é bastante adequado para organizarmos um dicionário letterCountcom a frequência de ocorrência de letras em nossa frase de várias linhas:

 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, obteremos um dicionário letterCount [Character : Int]no qual as chaves keysão os caracteres encontrados na frase em estudo e como o valor valueé o número desses caracteres.

O segundo método envolve inicializar o dicionário usando o agrupamento, que fornece o mesmo resultado:

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

Gostaríamos de classificar o dicionário em letterCountordem alfabética:

 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"] 

Mas não podemos classificar o dicionário diretamente Dictionary, pois não é fundamentalmente uma estrutura de dados ordenada. Se aplicarmos a função sorted (by:)ao dicionário Dictionary, ele retornará para nós os elementos da sequência classificada com o predicado fornecido na forma de uma matriz de tuplas nomeadas, que maptransformamos em uma matriz de seqüências de caracteres que [":17", ":5", ":18", ...]refletem a frequência de ocorrência da letra correspondente.

Vemos que desta vez sorted (by:)apenas o operador " <" é passado como predicado para uma função de ordem superior . A função sorted (by:)espera uma "função de comparação" como o único argumento na entrada. É usado para comparar dois valores adjacentes e decidir se eles estão ordenados corretamente (neste caso, retornatrue) ou não (retorna false). Podemos atribuir a essa "função de comparação" funções sorted (by:)na forma de um fechamento anônimo:

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

E podemos apenas fornecer o operador " <", que possui a assinatura de que precisamos, como foi feito acima. Essa também é uma função e a classificação por chave está em andamento key.

Se queremos classificar o dicionário por valores valuee descobrir quais letras são mais frequentemente encontradas nesta frase, teremos que usar o fechamento da função 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"] 

Se dermos uma olhada na solução para o problema de determinar o espectro de letras de uma frase multilinha como um todo ...

 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"] 

… , ( var , let) () , , :

split - ,
map
flatMap - ( ),
filter - ,
sorted - ,
reduce -

, . «» , map , , flatMap, se quisermos selecionar apenas determinados dados, usaremos filteretc. Todas essas funções de "ordem mais alta" são projetadas e testadas Applelevando em consideração a otimização do desempenho. Portanto, esse trecho de código é muito confiável e conciso - não precisávamos de mais de cinco frases para resolver nosso problema. Este é um exemplo de programação funcional.

A única desvantagem da aplicação da abordagem funcional nesta demonstração é que, por uma questão de imutabilidade, testabilidade e legibilidade, perseguimos repetidamente nosso texto por várias funções de ordem superior. No caso de um grande número de itens de coleção, o Collectiondesempenho pode despencar. Por exemplo, se usarmos primeiro filter(_:)e, e depois - first.
EmSwift 4 Algumas novas opções de recursos foram adicionadas para melhorar o desempenho, e aqui estão algumas dicas para escrever código mais rápido.

1. Use contains, NÃOfirst( where: ) != nil


A verificação de que um objeto está em uma coleção Collectionpode ser feita de várias maneiras. O melhor desempenho é fornecido pela função contains.

CÓDIGO CORRETO

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

CÓDIGO INCORRETO

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

2. Use validação isEmpty, NÃO uma comparação countcom zero


Como em algumas coleções, o acesso à propriedade counté realizado através da iteração sobre todos os elementos da coleção.

CÓDIGO CORRETO

 let numbers = [] numbers.isEmpty 

CÓDIGO INCORRETO

 let numbers = [] numbers.count == 0 

3. Verifique a string vazia StringcomisEmpty


String Stringin Swifté uma coleção de caracteres [Character]. Isso significa que para strings Stringtambém é melhor usar isEmpty.

CÓDIGO CORRETO

 myString.isEmpty 

CÓDIGO INCORRETO

 myString == "" myString.count == 0 

4. Obtenção do primeiro elemento que satisfaz certas condições


A iteração de toda a coleção para obter o primeiro objeto que satisfaz determinadas condições pode ser executada usando um método filterseguido por um método first, mas o método é o melhor em termos de velocidade first (where:). Esse método para de percorrer a coleção assim que ela atende às condições necessárias. O método filtercontinuará a percorrer toda a coleção, independentemente de ter atendido aos elementos necessários ou não.

Obviamente, o mesmo vale para o método last (where:).

CÓDIGO CORRETO

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

CÓDIGO INCORRETO

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

Às vezes, quando a coleção Collectioné muito grande e o desempenho é crítico para você, vale a pena comparar as abordagens imperativas e funcionais e escolher a que mais lhe convém.

TAREFA 2


Há outro ótimo exemplo de um uso muito conciso de uma função de ordem superior reduce (_:, _:)que me deparei. Este é um jogo SET .
Aqui estão suas regras básicas. O nome do jogo SETvem da palavra em inglês "set" - "set". O jogo SETenvolve 81 cartas, cada uma com uma imagem única:



Cada carta possui 4 atributos, listados abaixo:

Quantidade : cada carta possui um, dois ou três caracteres.
Tipo de caracteres : ovais, losangos ou ondas.
Cor : os símbolos podem ser vermelhos, verdes ou roxos.
Preenchimento : os caracteres podem estar vazios, sombreados ou sombreados.

Objetivo do jogoSET : 12 , , SET (), 3- , , 3- . .

, 3- , , 3- , , …

SET struct SetCard SET 3- 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) }) } } 

number , shape , color fillVariant , 3 : var1 , var2 var3 , 3- rawValue1,2,3 . rawValue . - , , color , rawValue colors 3- , , colors 3- , 3 , 6 9 , , 6 . 3- rawValue colors 3- . , , 3 SET . , 3 SET , SetCard - number , shape , color fillrawValue 3-.

static isSet( cards:[SetCard]) sums rawValue 3- 4- reduce , 0 , {$0 + $1.number.rawValue} , {$0 + $1.color.rawValue} , {$0 + $1.shape.rawValue} , { {$0 + $1.fill.rawValue} . sums 3-, reduce , , true " AND " {$0 && ($1 % 3) == 0} . Swift 5 isMultiply(of:) % . : { $0 && ($1.isMultiply(of:3) } .

, 3 SetCard SET -, " " , Playground :



SET ( UI ) , .


. . , (, ) (, ). .

Swift point.free " Functions " " Side Effects " , " " « » .


Em um sentido matemático, isso significa aplicar uma função ao resultado de outra função. Em uma Swiftfunção, eles podem retornar um valor que você pode usar como entrada para outra função. Esta é uma prática de programação comum.

Imagine que temos uma matriz de números inteiros e queremos obter uma matriz de quadrados de números pares únicos na saída. Normalmente, reimplementamos isso da seguinte maneira:

 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] 

, , . ( ) , ( ) . — inegerArray , unique , — even , , square .

«» >>> |> , , integerArray «» :

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

F# , Elixir Elm «» .

Swift «» >>> |> , Generics , ( closure ) infix :

 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) } 

, , . , map «» >>> , 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"] 

Mas nem sempre uma abordagem funcional produz um efeito positivo.

No início, quando apareceu Swiftem 2014, todos correram para escrever bibliotecas com operadores para "composição" de funções e resolver uma tarefa difícil para a época, como analisar JSONusando operadores de programação funcional em vez de usar construções infinitamente aninhadas if let. Eu mesmo traduzi o artigo sobre a análise funcional do JSON que me encantou com sua solução elegante e era fã da biblioteca Argo .

Mas os desenvolvedores Swiftseguiram um caminho completamente diferente e propuseram, com base na tecnologia orientada a protocolos, uma maneira muito mais concisa de escrever código. Para "entregar" os JSONdados diretamente aoO suficiente para fazer isso Codable, que implementa automaticamente este protocolo, se o seu modelo é composto pelas conhecidas Swiftestruturas de dados: String, Int, URL, Array, Dictionary, etc.

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

Tendo JSONdados desse famoso artigo ...
 [ { "id" : 73, "name" : "Bloxus test", "url" : "http://remote.bloxus.com/" }, { "id" : 74, "name" : "Manila Test", "url" : "http://flickrtest1.userland.com/" } ] 

... no momento, você só precisa de uma linha de código para obter uma variedade 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 mundo se esqueceu com segurança de usar os operadores da "composição" de funções para analisar JSON, se houver outra maneira mais compreensível e fácil de fazer isso usando protocolos.

Se tudo é tão fácil, podemos "enviar" JSONdados para modelos mais complexos. Suponha que tenhamos um arquivo de JSONdados que tenha um nome user.jsone esteja localizado em nosso diretório.Ele Resources.contém dados sobre um determinado usuário:

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

E temos um Codable usuário Usercom um inicializador a partir dos dados 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 facilmente obter um novo usuário newUsercom um código funcional ainda mais simples:

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

, newUser Optional , User? :



, Resources invoices.json .

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

, User . struct Invoice

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

JSON invoices , 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) } 

invoices [Invoice]? :



user invoices , nil , , , UserEnvelope , :

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

Em vez de executar duas vezes if let...

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

... vamos escrever um análogo funcional do duplo if letcomo uma Genericfunção auxiliar zipque converte dois Optionalvalores em uma Optionaltupla:

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

Agora não temos motivos para atribuir algo às variáveis newUsere invoices, apenas incorporamos tudo à nossa nova função zip, usamos o inicializador UserEnvelope.inite tudo 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]) 

Em uma única expressão, um algoritmo inteiro para fornecer JSONdados a um complexo na forma de uma estrutura é compactado struct UserEnvelope.

  • zip , , . user , JSON , invoices , JSON . .
  • map , , «» .
  • flatMap , , , .

zip , map flatMap - (domain-specific language, DSL) .

, pointfree.co .

, .


Swf t « », map , flatMap , reduce , filter Sequence , Optional Result . « » , value struct enum . iOS .

, Playground , Github . Playground , :

«» Xcode Playground «Launching Simulator» «Running Playground».

Referências:

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/pt440722/


All Articles