Implementa le operazioni di base sugli array.
Nei linguaggi funzionali, le operazioni sugli array come length, map e reduce sono molto comuni.
Implementa una serie di operazioni di base sugli array, senza usare le funzioni esistenti.
Il numero preciso e i nomi delle operazioni da implementare dipenderà dal track, per evitare conflitti con i nomi già esistenti, ma le operazioni generali che implementerai includono:
append (dati due array, aggiungi tutti gli elementi del secondo array alla fine del primo array);concatenate (data una serie di array, combina tutti gli elementi di tutti gli array in un unico array appiattito);filter (dati un predicato e un array, restituisci l'array di tutti gli elementi per cui predicate(item) è True);length (dato un array, restituisci il numero totale di elementi che contiene);map (date una funzione e un array, restituisci l'array dei risultati dell'applicazione di function(item) a tutti gli elementi);foldl (date una funzione, un array e un accumulatore iniziale, piega (riduci) ogni elemento nell'accumulatore da sinistra);foldr (date una funzione, un array e un accumulatore iniziale, piega (riduci) ogni elemento nell'accumulatore da destra);reverse (dato un array, restituisci un array con tutti gli elementi originali, ma in ordine inverso).Nota: l'ordine in cui gli argomenti vengono passati alle funzioni fold (foldl, foldr) è significativo.
Per questo esercizio dovrai usare il polimorfismo dei parametri di Odin (più comunemente chiamato generics). Se non hai mai visto questa funzionalità, ecco una breve introduzione per iniziare.
Il polimorfismo dei parametri è una funzionalità dei linguaggi di programmazione che permette a chi scrive codice di essere meno specifico (più generico, da cui il nome) riguardo ai tipi usati, mantenendo comunque la sicurezza dei tipi. Ovviamente questo ha senso solo per i linguaggi fortemente tipizzati come Odin.
Iniziamo con un esempio. Diciamo che vuoi incrementare tutti gli elementi di un array di un valore fisso, un problema abbastanza semplice.
incr_array_int :: proc(a: []int, by: int) -> []int {
new_array := make([]int, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
E se ora ti servisse la stessa funzionalità per i numeri in virgola mobile?
incr_array_f64 :: proc(a: []f64, by: f64) -> []f64 {
new_array := make([]f64, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
E poi per gli interi senza segno, i numeri in virgola mobile a 32 bit e così via?
In poco tempo ti ritrovi con una miriade di procedure che svolgono esattamente la stessa funzione ma su tipi diversi. Se un giorno devi aggiornare la logica, devi assicurarti di farlo per tutte le varianti: può diventare un bel po' di lavoro di manutenzione. L'altro fastidio è che devi dare nomi diversi a ciascuna procedura, dato che Odin non supporta l'overloading implicito delle procedure (puoi comunque usare l'overloading esplicito, ma questa è una storia per un altro esercizio).
Odin, essendo un linguaggio pratico, offre una soluzione con il polimorfismo dei parametri.
Purché il compilatore riesca a capire il tipo di un parametro in fase di compilazione, puoi dargli un nome generico, come T.
Riscriviamo la procedura qui sopra:
incr_array :: proc(a: []$T, by: T) -> []T {
new_array := make([]T, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
Nota che abbiamo sostituito tutte le annotazioni di tipo (int o f64) con T e che la prima occorrenza di T è preceduta da un simbolo del dollaro ($T).
Il tipo $T dice al compilatore di Odin che il nome T è un nome generico per il tipo, da sostituire con il nome effettivo durante la compilazione.
E, dato che ora il compilatore conosce il tipo generico T, le occorrenze successive dello stesso tipo devono essere contrassegnate solo con il nome di tipo scelto (T).
Ora puoi scrivere codice come:
a_int := incr_array([]int{1, 2, 3}, 10)
a_f64 := incr_array([]f64{1.0, 2.0, 3.0}, 10.0)
Nella prima istruzione, il compilatore di Odin identificherà il tipo del primo parametro ([]int) con il tipo del parametro generico ([]$T), dedurrà che T = int e procederà a compilare una versione con tutte le successive istanze di T sostituite da int (equivalente alla versione specializzata incr_array_int() qui sopra).
Se omettessi il simbolo del dollaro nella definizione del primo parametro, il compilatore cercherebbe nel pacchetto corrente e nell'elenco degli import un tipo chiamato T e molto probabilmente restituirebbe un errore di compilazione: Error: Undeclared name: T.
La seconda istruzione funziona esattamente come la prima, ma con il compilatore che identifica T come f64.
È consuetudine dare ai tipi generici nomi di una sola lettera (T ed E sono quelli usati più spesso).
Ora dovresti saperne abbastanza sui polimorfismi dei parametri, ovvero i tipi generici, per affrontare l'esercizio Operazioni sulle liste.
Iscriviti a Exercism per imparare e padroneggiare Odin con 73 esercizi e il mentoring di persone reali, tutto gratis.
Goditi un'introduzione pratica alla ricorsione, esplora le alternative imperative e funzionali a Operazioni su array e approfondisci la ricorsione in coda e le funzioni di accumulo.