Heroes / Guida / capitolo 8

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
examples/gallery/06-generics.hero, righe 8–16

<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

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()
examples/gallery/06-generics.hero, righe 36–47

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
examples/gallery/06-generics.hero, righe 18–25

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
examples/gallery/06-generics.hero, righe 27–34

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
examples/gallery/06-generics.hero, righe 49–53

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))
examples/gallery/06-generics.hero, righe 55–56
$ 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.