Generici e valori funzione
Una funzione può lavorare con tipi diversi e ricevere altre funzioni come argomenti.
Una funzione generica può eseguire la stessa operazione su tipi diversi. Per esempio, puoi scrivere una sola funzione per trasformare gli elementi di una lista, sia che contenga numeri sia che contenga stringhe. I tipi concreti vengono determinati dagli argomenti della chiamata.
Dichiarare i parametri di tipo
# The parentheses around a function type are mandatory: without them
# `f: function(A) -> B -> [B]` would be a guessing game.
function each<A, B>(xs: [A], f: (function(A) -> B)) -> [B]
out: [B] @= []
for x in xs
out @ out.push(f(x))
return out
<A, B> dichiara due parametri di tipo. Il compilatore determina i tipi concreti dagli argomenti della chiamata. Nel corpo, [A] indica una lista di elementi di tipo A e out: [B] @= [] dichiara una lista vuota di elementi di tipo B. La lista vuota richiede un tipo esplicito perché non contiene elementi da cui dedurlo.
Le tre regole delle funzioni generiche
-
Solo le funzioni possono essere generiche. Un campo di un
recordo di unvariantha un tipo concreto, erecord Box[T]non esiste. Le liste e le mappe prendono un tipo di elemento, e ognuna contiene un solo tipo alla volta:ys = [1, "a"]dàerror[type_mismatch]. Per tenere valori di tipi diversi in una sola lista, dichiara unvariante fai una lista di quello. - Nessuna condizione sui tipi. Non puoi dire “un
Aqualsiasi, purché si possa confrontare”. Se alla funzione serve un’operazione suA, quell’operazione gliela passi come argomento, esattamente comefqui sopra. -
I tipi non si scrivono nella chiamata. Il compilatore li deduce dagli argomenti. Se non può farlo, segnala un errore. La sintassi
each<i64, str>(…)non è ammessa.
Scrivi quindi each con i soli argomenti: i loro tipi forniscono al compilatore le informazioni necessarie.
Le funzioni sono valori, e hanno un nome
# The functions passed above are ordinary declarations. No lambdas exist.
function double(n: i64) -> i64
return n * 2
function is_positive(n: i64) -> bool
return n > 0
function add(a: i64, b: i64) -> i64
return a + b
function label(n: i64) -> str
return n.to_str()
Il nome di una funzione dichiarata al livello principale del file, scritto senza parentesi, è un valore che puoi passare a un’altra funzione. Heroes non ha lambda, cioè funzioni anonime, né funzioni che conservano le variabili del contesto in cui sono definite. Ogni funzione ha quindi un nome e una dichiarazione che puoi leggere, testare e riutilizzare.
Non è un rinvio, è una decisione presa. Le funzioni anonime sono state esaminate e rifiutate, e quello che ha chiuso la questione non è un argomento ma una misura: sotto, una funzione che si passa in giro è un solo indirizzo di memoria, cioè un numero che dice dove il codice si trova, ed è esattamente quello che una libreria C si aspetta di ricevere. Una funzione che si portasse dietro dei valori occuperebbe diverse volte tanto. Provata contro una libreria C vera, con tutti i controlli del compilatore accesi, la compilazione non ha segnalato nulla e il programma è andato in crash appena eseguito.
Il guadagno, misurato, era piccolo. Nel compilatore di Heroes, che è scritto in Heroes e conta più di cinquantamila righe, una funzione viene passata a un’altra in meno di cinque punti. Le funzioni di una riga con un nome che una lambda avrebbe cancellato sono meno di venticinque e stanno tutte nei programmi d’esempio, e più di metà dei file che le contengono sono proprio gli esempi il cui argomento è questo: sono la dimostrazione, non il fastidio. E non è una scelta isolata: altri linguaggi costruiti con gli stessi vincoli rifiutano la stessa cosa e lo scrivono nella loro documentazione. È uno dei no a registro, con chi l’ha detto e che cosa lo smentirebbe: i no di Heroes →
Il tipo di una funzione si scrive (function(A) -> B). Le parentesi esterne sono obbligatorie: senza di esse, f: function(A) -> B -> [B] sarebbe ambiguo. Il compilatore segnala l’errore:
$ heroes check generics.hero
error[expected_type]: expected a type, found `function` — a type is `i64`, `f64`, `bool`, `str`, `[T]`, `{K: V}`, `T?`, `()`, or the name of a `record` or `variant`
at generics.hero:18:38
|
18 | function keep_if<A>(xs: [A], ok_for: function(A) -> bool) -> [A]
| ^^^^^^^^
Filtrare e combinare gli elementi
function keep_if<A>(xs: [A], ok_for: (function(A) -> bool)) -> [A]
out: [A] @= []
for x in xs
if ok_for(x)
out @ out.push(x)
return out
Questa funzione ha un solo parametro di tipo perché il filtro mantiene il tipo degli elementi. Se passi una funzione con tipi incompatibili, il messaggio indica l’argomento errato e i due tipi:
$ heroes check generics.hero
error[type_mismatch]: expected `bool`, found `i64`
at generics.hero:52:36
|
52 | assert keep_if([-1, 2, -3, 4], double) == [2, 4]
| ^^^^^^
# Two type parameters, and the accumulator is the one that names the result.
function combine<A, B>(xs: [A], start: B, step: (function(B, A) -> B)) -> B
total: B @= start
for x in xs
total @ step(total, x)
return total
combine percorre una lista di A e calcola un risultato di tipo B. Cambiando la funzione step puoi sommare numeri, unire stringhe o contare gli elementi che soddisfano una condizione.
Un generico solo, tre tipi di elemento
test "one generic, three element types"
assert each([1, 2, 3], double) == [2, 4, 6]
assert each([1, 2], label) == ["1", "2"]
assert keep_if([-1, 2, -3, 4], is_positive) == [2, 4]
assert combine([1, 2, 3, 4], 0, add) == 10
Se passi a each una funzione che restituisce str, il risultato è una lista [str]. In entrambe le chiamate il compilatore deduce e verifica i tipi.
Il compilatore genera una versione di each per ogni combinazione di tipi concreti usata dal programma. Il codice C contiene quindi versioni separate per gli interi e per le stringhe. Il programma non deve scegliere i tipi durante l’esecuzione né convertire i valori in un contenitore generico. Le versioni aggiuntive aumentano però la dimensione dell’eseguibile.
function main()
print(combine([1, 2, 3, 4], 0, add))
$ heroes run examples/gallery/06-generics.hero
10
Le funzioni map, filter, fold, find, any e all sono già disponibili e sono scritte in Heroes. Le tre funzioni di questo capitolo ne illustrano il funzionamento con nomi diversi, perché non puoi ridefinire un nome predefinito.
Poi: Le stringhe, per distinguere i byte dai caratteri e scegliere le operazioni corrette. Torna all’elenco dei capitoli. ← Prima: Nomi e modifiche
Esegui un programma tuo: bastano quattro comandi. Oppure prendi tutto il linguaggio in un file solo, e tutti i programmi già scritti.