
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.
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:
- O que você está tentando alcançar com seu código?
- O que acontece se outro
thread
tentar acessar a matriz de numbers
enquanto seu código estiver em execução? - O que acontece se você deseja ter acesso aos valores originais na matriz de
numbers
? - Quão confiável esse código pode ser testado?
Agora vamos ver uma abordagem alternativa "
Funcional ":
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.
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.
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.
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)
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)

Sempre que você usar o
init?()
Como a função de transformação, será necessário usar o
compactMap(_ :)
:
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?()
“
failable
”
init?()
, 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
.
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)
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)
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.offset
não está contido na matriz removedIndexes
. A próxima etapa, selecionamos o valor da tupla $0.element
e 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 Sequence
para 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 Sequence
para 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)
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
Nesta demonstração, o valor acumulado é
GCRect
e a operação acumulativa é a operação de combinar os union
retângulos que são frame
nossos 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:
Swift 4.2
introduziu 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, Array
ou for usado como a estrutura resultante Dictionary
.Ele pode ser usado efetivamente para remover valores correspondentes em uma matriz de números inteiros:
... ou ao contar o número de elementos diferentes em uma matriz:
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 firstNumber
será igual 42
:
mas, se você não souber, Playground
muitas vezes oculta o verdadeiro
, em particular as
constantes firstNumber
. De fato, a constante firstNumber
tem
duas coisas Optional
:
Isso ocorre porque, map (Int.init)
na saída, forma uma matriz Optional
de valores TYPE [Int?]
, pois nem todas as linhas String
podem ser convertidas Int
e o inicializador Int.int
está "caindo" ( failable
). Em seguida, pegamos o primeiro elemento da matriz formada usando a função first
da 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 Optional
na Optional
qual é 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 Optional
ainda mais o nível .Obter o valor do aninhado duplo é Optional
realmente 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 words
tem 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 Array
e transformá-la em uma única matriz Array
. Isso é semelhante ao que fizemos com o dobro Optional
de 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 words
sem nenhum "aninhamento": ["", ",", "", ",", "", "", ";", "", "", "", "", ",", "—", "", ",", "", "", ":", "", "—", "", "", ",", "", "", ".", "", "", ",", "", "", ",", "", "", ".", "", "", "", ".", "", ",", "", "", "", "", "", "", "", ",", "", "«", "»", "", ".", "", ",", "", ",", "", "", "!"]
Agora podemos continuar o processamento que precisamos da matriz de palavras resultante words
, mas tenha cuidado. Se o aplicarmos novamente flatMap
a 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 Collection
caracteres [Character]
e, aplicando-se flatMap
a 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 words
e 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 words
em 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.2
variedade de uma função de ordem superior que apareceu reduce (into:, _:)
. Este método é bastante adequado para organizarmos um dicionário letterCount
com 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)
Como resultado, obteremos um dicionário letterCount
[Character : Int]
no qual as chaves key
sã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
Gostaríamos de classificar o dicionário em letterCount
ordem alfabética: let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat)
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 map
transformamos 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 value
e 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 )
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)
… , (
var
,
let)
() , , :
split
- ,
map
—
flatMap
- ( ),
filter
- ,
sorted
- ,
reduce
-
, . «» ,
map
, ,
flatMap
, se quisermos selecionar apenas determinados dados, usaremos filter
etc. Todas essas funções de "ordem mais alta" são projetadas e testadas Apple
levando 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 Collection
desempenho 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 Collection
pode 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 count
com 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 String
comisEmpty
String String
in Swift
é uma coleção de caracteres [Character]
. Isso significa que para strings String
també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 filter
seguido 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 filter
continuará 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 SET
vem da palavra em inglês "set" - "set". O jogo SET
envolve 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
,
shape
,
color
fill
—
Variant
, 3 :
var1
,
var2
var3
, 3-
rawValue
—
1,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
fill
—
rawValue
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 Swift
funçã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)))
, , . ( ) , ( ) . —
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)
Mas nem sempre uma abordagem funcional produz um efeito positivo.No início, quando apareceu Swift
em 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 JSON
usando 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 Swift
seguiram 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 JSON
dados diretamente ao
O suficiente para fazer isso
Codable
, que implementa automaticamente este protocolo, se o seu modelo é composto pelas conhecidas Swift
estruturas de dados: String
, Int
, URL
, Array
, Dictionary
, etc. struct Blog: Codable { let id: Int let name: String let url: URL }
Tendo JSON
dados 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!)")
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" JSON
dados para modelos mais complexos. Suponha que tenhamos um arquivo de JSON
dados que tenha um nome user.json
e 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 User
com 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 newUser
com 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 let
como uma Generic
função auxiliar zip
que converte dois Optional
valores em uma Optional
tupla: 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 newUser
e invoices
, apenas incorporamos tudo à nossa nova função zip
, usamos o inicializador UserEnvelope.init
e 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!)")
Em uma única expressão, um algoritmo inteiro para fornecer JSON
dados 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 3Inside the Standard Library: Sequence.map()Practical functional programming in Swift