Da quanto illustrato fino ad ora, è chiaro che Swift è un linguaggio di programmazione orientato agli oggetti, sia per il fatto di esser stato progettato per sostituire Objective-C sia per renderlo adatto al framework Cocoa, usato per lo sviluppo di app per iOS e OS X.
Tuttavia i designer di Swift hanno pensato di introdurre nel linguaggio dei costrutti che permettessero allo sviluppatore di adottare altri stili di programmazione, in particolare quello funzionale.
Nello stile di programmazione orientato agli oggetti, il programmatore usa gli oggetti per astrarre i problemi e fornire le relative soluzioni, mentre nello stile funzionale, l'unita base di astrazione è la funzione.
Swift è dunque un linguaggio di programmazione ibrido, dato che supporta sia lo stile object-oriented che quello funzionale.
Principi della programmazione funzionale
Alla base della programmazione funzionale sta il concetto di uso delle funzioni nel senso matematico del termine.
Se consideriamo lo spazio percorso x da un oggetto che si muove con una velocità costante v in un dato tempo t, possiamo determinare univocamente x con una funzione matematica:
x = v * t
In Swift questo verrebbe tradotto in:
func spazioPercorso(v: Double, t: Double) -> Double {
return v * t
}
La funzione spazioPercorso
restituirà sempre il medesimo valore per gli stessi input v
e t
.
Consideriamo ora la seguente funzione Swift:
func spazioPercorso(v: Double, t: Double) -> Double {
let random = arc4random_uniform(2) + 1
return v * t * Double(random)
}
Questa funzione Swift non restituisce sempre il medesimo risultato per le stesse coppie di valori di input. Questa dunque non modella una funzione matematica, pertanto tale funzione non è conforme allo stile di programmazione funzionale, in cui il risultato di una funzione è sempre ben determinato dai parametri di input, così come avviene per le funzioni matematiche.
Il secondo principio della programmazione funzionale è basato sul concetto di immutabilità. Questo in Swift si traduce nel preferire l'uso delle costanti (definite con let
) alle variabili, in modo da modellare il concetto delle variabili matematiche:
y = 3
in Swift:
let y = 3
Il terzo principio è basato sull'utilizzo delle funzioni di ordine superiore (high order function): si tratta di funzioni che accettano altre funzioni come parametri, trattando quindi le funzioni come valori. Le closure in Swift ci permettono di implementare questa terza pratica.
Queste tre pratiche permettono di scrivere codice che risulta lineare e ben deterministico, principi alla base dello stile di programmazione funzionale.
Funzioni di ordine superiore: map, filter e reduce
Swift include 3 funzioni di ordine superiore presenti in tutti i linguaggio funzionali: map
, filter
e reduce
.
La funzione map
opera su una collezione di oggetti, applicando ad ogni singolo elemento una trasformazione, e restituendo una nuova collezione con gli oggetti trasformati.
Supponiamo ad esempio di avere una collezione di nomi, rappresentata in Swift da un array di String
:
var names = ["antonio", "filippo", "salvatore", "simone"]
Se volessimo convertire ogni elemento della collezione in lettere maiuscole potremmo utilizzare il seguente codice:
var capitalNames: [String] = []
for name in names {
capitalNames.append(name.uppercaseString)
}
Se volessimo risolvere il precedente problema con un approccio funzionale, potremmo utilizzare map
:
var upperNames = names.map({ (name: String) -> String in return name.uppercaseString })
map
è un metodo automaticamente disponibile su tutte i tipi che implementano il protocollo SequenceType
, tra cui Array
. Esso richiede un solo parametro, una funzione di trasformazione da applicare ad ogni elemento. Nell'esempio precedente abbiamo utilizzato una closure. Possiamo semplificare ulteriormente la sintassi del precedente codice nei seguenti modi:
var upperNames2 = names.map({ (name: String) -> String in name.uppercaseString })
var upperNames3 = names.map({ (name: String) in name.uppercaseString })
var upperNames4 = names.map({ name in name.uppercaseString })
var upperNames5 = names.map({ $0.uppercaseString })
var upperNames6 = names.map() { $0.uppercaseString }
sfruttando il meccanismo di type inference e della trailing closure illustrata nella prima parte di questo articolo.
Il metodo filter
permette di filtrare una collezione di elementi in base al risultato di un predicato funzionale: una funzione che accetta un singolo elemento come parametro e restituisce true
o false
. filter
applica il predicato ad ogni singolo elemento della collezione, restituendo una nuova collezione con i soli elementi il cui predicato ritorna true
:
var namesWithA = upperNames6.filter({$0.containsString("A")})
namesWithA
è un nuovo array con i soli nomi che contengono la lettera "A" maiuscola.
map
e filter
permettono di trasformare una collezione in una nuova collezione. reduce
, invece, permette di trasformare una collezione in qualsiasi altro tipo. reduce
richiede come parametro una funzione capace di combinare un elemento della collezione di input con un risultato intermedio. Ogni singolo elemento della collezione verrà combinato con il risultato dell'iterazione precedente, e il valore finale verrà restituito al termine di tutte le iterazioni.
reduce
accetta due parametri di input: un valore iniziale e una funzione di combinazione.
Se dovessimo sommare i valori numerici di un array di interi, usando il metodo reduce
potremmo scrivere quanto segue:
var numeri = [1,2,3,4,5]
var somma = numeri.reduce(0, combine: {$0 + $1})
o ancora più brevemente:
var somma2 = numeri.reduce(0, combine: +)
Con reduce
possiamo riscrivere codice che fa uso di map
o filter
. Ad esempio, la seguente funzione:
func namesWithLetter(letter: String, list: [String]) -> [String] {
return list.reduce([], combine: {
$0 + ($1.containsString(letter) ? [$1]: [])
})
}
fa uso del metodo reduce
per implementare una generica funzione che filtra i nomi di un array che contengono una data lettera:
namesWithLetter("A", list: upperNames4)
che in precedenza avevamo scritto con il metodo filter
.