{"id":22945216,"url":"https://github.com/curiousci/keras-library","last_synced_at":"2025-04-01T21:44:04.136Z","repository":{"id":208729257,"uuid":"719490931","full_name":"CuriousCI/keras-library","owner":"CuriousCI","description":"Introduction to programming homework","archived":false,"fork":false,"pushed_at":"2023-11-25T18:05:24.000Z","size":556,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-07T14:25:01.761Z","etag":null,"topics":["fsm","problem-solving","rust"],"latest_commit_sha":null,"homepage":"https://q2a.di.uniroma1.it/29558/hw-homework-4-obbligatorio-prima-scadenza-ore-23-59-del-18-11","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CuriousCI.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-16T09:29:28.000Z","updated_at":"2023-12-15T20:41:08.000Z","dependencies_parsed_at":"2023-11-25T10:25:38.291Z","dependency_job_id":"b58183b8-0f56-4e84-8ac6-4cd4903521a5","html_url":"https://github.com/CuriousCI/keras-library","commit_stats":null,"previous_names":["curiousci/keras-library"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CuriousCI%2Fkeras-library","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CuriousCI%2Fkeras-library/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CuriousCI%2Fkeras-library/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CuriousCI%2Fkeras-library/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CuriousCI","download_url":"https://codeload.github.com/CuriousCI/keras-library/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246716878,"owners_count":20822535,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fsm","problem-solving","rust"],"created_at":"2024-12-14T14:30:05.784Z","updated_at":"2025-04-01T21:44:04.119Z","avatar_url":"https://github.com/CuriousCI.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Keras Library _(HW4)_\n\nE la storia di come ho ottimizzato la mia soluzione rendendola **8 volte più veloce**. La soluzione più veloce in classifica impiega **41ms** per eseguire tutti i test, la mia ne impiega **5**\n\n\u003c!-- TODO: citare le specifiche delle macchine su cui ho eseguito i test --\u003e\n\n\n## Python 🗑️!\n\n![C level meme](C-level.png)\n\n\u003c!-- TABELLA NUMERO FILE E DURATE CANZONI (media e totale) --\u003e\n\n\nPer i poveretti che hanno dovuto affrontare l'HW4 quest'anno il test più problematico è stato il `test10` per il _Timeout_, come mai? Se si fa una veloce analisi sulla struttura dei test, si scopre che il `test10` ha 181 file e 4919914 caratteri da convertire _(quasi 5 milioni 💀)_. In confronto, il `test03` ha 610 file e 419723 caratteri da convertire in totale _(\"solo\" 420 mila 🌿)_.\n\n| test | # file | # totale caratteri nei file | \n|--|--|--|\n| test01 | 18 | 1513 |\n| test02 | 15 | 1609 |\n| test03 | 613 | 419723 |\n| test04 | 11 | 9130 |\n| test05 | 34 | 28293 |\n| test06 | 25 | 5651 |\n| test07 | 18 | 125139 |\n| test08 | 17 | 35491 |\n| test09 | 11 | 6249 |\n| test10 | 181 | 4919914 |\n\nIl `test10` impiega molto più tempo del `test03`, il che, seppur possa sembrare ragionevole, sta mettendo in luce il fatto che l'**algoritmo è tanto lento quanto le operazioni di lettura e scrittura sul disco**. Come si scopre dai benchmark della mia soluzione, il `test03` ci mette il doppio rispetto al `test10` _(senza thread)_, perché in un linguaggio **normale** la traduzione _(in RAM)_ dovrebbe essere **estremamente più veloce** rispetto all'interazione con il disco rigido _(per creare una marea di cartelle e leggere / scrivere una marea di file)_\n\nHo deciso di usare [Rust](https://www.rust-lang.org/) siccome volevo farci un po' di pratica, è un linguaggio allo stesso livello di C ed è piacevole lavorarci _(perché ha astrazioni come quelle di Python)._\n\n\u003e Don't worry, no snake was harmed during the writing of this post\n\n## Traduzione burlona... \n\nOk, inizialmente volevo discutere la parte della traduzione _(perché le altre ottimizzazioni sono più \"difficili da spiegare\")_. Il primo in classifica ha scritto una cosa del genere:\n\n```python\nZIPPED_REPLACE_MAP = (\n    (b'\\x00-', b'\\x07'), (b'\\x01-', b'\\x08'),\n    (b'\\x02-', b'\\x09'), (b'\\x03-', b'\\x0A'),\n    (b'\\x04-', b'\\x0B'), (b'\\x05-', b'\\x0C'),\n    (b'\\x06-', b'\\x0D'), (b'\\x06+', b'\\x14'),\n    (b'\\x00+', b'\\x0E'), (b'\\x01+', b'\\x0F'),\n    (b'\\x02+', b'\\x10'), (b'\\x03+', b'\\x11'),\n    (b'\\x04+', b'\\x12'), (b'\\x05+', b'\\x13')\n)\n\ndef tknz_tarahumara(path: str, out: str) -\u003e int:\n    with open(path, \"r\") as inp:\n        content = b\"\".join(x[::-1] for x in inp.buffer).translate(TABLE, b'\\n')\n    for t, r in ZIPPED_REPLACE_MAP:\n        content = content.replace(t, r)\n\n    tokens = 0\n    c = 0\n    last = content[0]\n\n    with open(out, \"w\") as out_f:\n        for tkn in content + b\"\\xff\":\n            if tkn is last:\n                c += 1\n                continue\n            out_f.write(f\"{local_conv[last]}{c}\")\n\n            last = tkn\n            tokens += c\n            c = 1\n    return tokens\n```\n\nL'algoritmo si può rappresentare con una [FSM](https://en.wikipedia.org/wiki/Finite-state_machine) a 2 stati molto semplice\n\n\u003cimg src=\"./fsm-small.svg\" width=370\u003e\n\nQual'è il problema di questa implementazione? Il fatto che le note alterate, `'Xb'`, sono composte da 2 caratteri. La soluzione più veloce _(in Python)_ per ovviare a questo problema è usare **14** `.replace()` per rimpiazzare tutte le **note alterate** con **byte singoli** (ad esempio: `'4+'`, `0x042b'` in esadecimale 2 byte, viene trasformato in `'0x12'`, in esadecimale 1 byte). \n\nQuando una sequenza viene salvata va riconvertita nella rispettiva nota con un **dizionario**.\n\n### L'alternativa?\n\nOk, ora pensiamo all'FSM alternativa senza l'utilizzo di `.replace()` e di `dict()` per le conversioni, quindi vanno considerati i caratteri `'b'` e `'#'` separatamente\n\n\u003cimg src=\"./fsm.svg\" width=500\u003e\n\nSembra complessa, ma si può implementare senza grandi difficoltà con qualche `if`, e il vantaggio è che permette di **iterare il testo 1 volta sola**, rispetto alle **14 iterazioni** che bisogna fare con i vari `.replace()`. Perché non usiamo questa soluzione? Perché l'implmentazione della seconda FSM in Python è molto più lenta rispetto ad usare i **14** `.replace()`, visto che usare **if** nel **for** in Python è un'operazione **LENTISSIMA!** Il vantaggio di `.replace()` è proprio quello di essere implementato in C, quindi non soffre quasi per nulla la lentezza di Python. \n\nPer fortuna, nei linguaggi come **Rust**, **C++** e **C**, il costo dell'**if** è quasi 0, per questo ho deciso di implementare la seconda FSM nella mia traduzione, ma come ho fatto? All'inizio, definiendo i possibili stati\n\n```Rust\n// Ho usato la terminologia inglese per *elencare* i tipi di note \n\nenum Note {\n    None,\n    Normal, // Nota semplice\n    Accidental, // Nota alterata (con b o #)\n    Unknown, // Nota: A? (non so se è semplice o alterata)\n}\n\n/* \n\"Unknown\" si verifica con la sequenza \"AbA\" perché\nnon so se l'ultima 'A' è semplice o alterata finché\nnon leggo il carattere dopo\n*/ \n```\n\nIl prossimo step è quello di leggere i dati dal file _(qui sopravvive solo la vesione migliore, se volete vedere le altre c'è la commit history del repository e ci sono i test)_\n\n```rust\nlet (translation, duration) = translate(\n\t\u0026read(source_folder.join(\u0026file))\n\t\t.unwrap()\n\t\t.iter()\n\t\t.map(|byte| match byte {\n\t\t\t10 =\u003e '\\n',\n\t\t\t32 =\u003e 'P',\n\t\t\t43 =\u003e '#',\n\t\t\t45 =\u003e 'b',\n\t\t\tbyte =\u003e (byte + 17) as char,\n\t\t})\n\t\t.collect(),\n);\n```\n\nCon `read(source_folder.join(\u0026file)).unwrap()` leggo i **byte** dal file della canzone e gestisco eventuali **errori di lettura**. Successivamente con `.iter().map(|byte| match byte ...)` non faccio altro che iterare su ogni singolo **byte** (quindi numero), e convertirlo nel rispettivo **carattere umkansaniano** e con `.collect()` trasformo i byte letti in un `Vec\u003cchar\u003e`, che sarebbe il corrispettivo di **List** in Rust _(grossolanamente)_.\n\nAdesso che ho una lista di caratteri su cui lavorare, posso implementare l'FSM!\n\n```rust\n// Ho tagliato alcuni pezzi, potete vedere il codice completo nel file src/thread/pool.rs\n\npub fn translate(score: \u0026Vec\u003cchar\u003e) -\u003e (String, i32) {\n    let mut song_duration = 0;\n    let mut note = ' ';\n    let mut accidental = ' ';\n    let mut duration = 0; // Durata della sequenza attuale\n    let mut note_type = None;\n    let mut translation = String::new();\n\n    for staff in score.split(|char| char == \u0026'\\n') {\n\t\t// 'score' è il termine inglese per spartito \n\t\t// 'staff' è il termine inglese per pentagramma \n\n        for \u0026symbol in staff.iter().rev() {\n\t\t\t// Ad ogni iterazione aggiorno lo stato aggiornando note_type\n\n            note_type = if symbol == note {\n                song_duration += 1;\n\n                match note_type {\n                    Normal =\u003e {\n                        duration += 1;\n                        Normal\n                    }\n                    Accidental =\u003e Unknown,\n                    Unknown =\u003e {\n                        duration = 2;\n                        Normal\n                    }\n                    _ =\u003e unreachable!(),\n                }\n            } else {\n                match symbol {\n                    '#' | 'b' =\u003e {\n                        match note_type {\n                            Normal =\u003e {\n                                duration = 1;\n                                accidental = symbol;\n                            }\n                            Unknown =\u003e {\n                                if symbol == accidental {\n                                    duration += 1;\n                                } else {\n                                    duration = 1;\n                                    accidental = symbol;\n                                }\n                            }\n                            _ =\u003e unreachable!(),\n                        };\n\n                        Accidental\n                    }\n                    _ =\u003e {\n                        song_duration += 1;\n                        note = symbol;\n                        duration = 1;\n                        Normal\n                    }\n                }\n            }\n        }\n    }\n\n    (translation, song_duration)\n}\n```\n\nCi sono alcune osservazioni interessanti:\n- Se il simbolo letto è uguale al precedente, il simbolo non può essere né `'b'` né `'#'`\n- Se il simbolo letto è diverso dal precedente e non è né `'b'` né `'#'` allora ricado sempre nello stato `Normal`, dato che ho una nota diversa dalla precedente\n\n\u003e Ho levato le righe di codice in cui salvo le sequenze, per cui non si vede che per aggiornare la traduzione uso la macro `write!()` che ho scoperto essere la strategia più veloce per aggiornare la stringa con un formato custom _(visto che convertire un numero intero in una sequenza di cifre è abbastanza costoso)_\n\nLa lettura dall'alto verso il basso e da destra verso sinistra è altrettanto semplice come in Python\n\n```rust\nfor staff in score.split(|char| char == \u0026'\\n') {\n\tfor \u0026symbol in staff.iter().rev() {\n\t}\n}\n```\n\nCon `.split()` divido lo **spartito** in **pentagrammi**, e con `.iter().rev()` inverto ogni pentagramma. Al posto di `.split(|c| c == \u0026'\\n')` avrei potuto usare semplicemente `.lines()`, ma è leggermente più lento perché controlla anche il carttere `'\\r'` o il fatto che ci sia `\"\\r\\n\"` \n\n\u003cimg src=\"./crlf-meme.png\" width=400\u003e\n\nL'altra cosa interessante è che mancano i `return`, perché di default, in Rust, se sono all'interno di un blocco parentesi graffe, e l'ultimo **statement** non ha il `;`, quello è il valore di ritorno del blocco.\n\n```rust\n_ =\u003e {\n\tsong_duration += 1;\n\tnote = symbol;\n\tduration = 1;\n\tNormal\n}\n```\n\nIn questo esempio il valore di ritorno del blocco `{}` è `Note::Normal`\n\nQuesta regola si può applicare un po' da tutte le parti, quindi posso fare cose interessanti come far ritornare un valore ad un blocco `if {} else {}` \n\n```rust\nlet avvertenza = if velocita \u003e limite {\n\t\"Stai superando il limite di velocità!\"\n} else if velocita \u003c= 10 {\n\t\"Muovi il c***!\"\n} else {\n\t\"Tutt'appost\"\n}\n```\n\n\u003e Per altre curiosità su Rust, il miglior posto è il [libro ufficiale](https://doc.rust-lang.org/book/)\n\n## Il pezzo forte: parallelismo ⚒️ \n\nNon ci basta essere più veloci di Python, vogliamo distruggerlo proprio 💣. Visto che i nostri PC hanno una marea di **core**, è giunto il momento di **sfruttarli tutti a pieno!**\n\nImmaginate di avere **600 documenti da tradurre**, e farli analizzare da una sola persona che li traduce uno alla volta... è logico che se assumo più persone possono tradurre i file contemporaneamente, quindi **finire prima il lavoro**. Supponiamo in maniera malsana di assumere **una persona per ogni documento**: questo è quello che ho fatto nella prima soluzione parallela, quella in `src/thread/mod.rs`.\n\nQuindi, nel `test03` creavo **613 thread** per tradurre i file, nel `test10` ne creavo **181** e così via. Per chi non sapesse cos'è un **thread**, deve immaginarlo come **una funzione che eseguo separatamente dal mio codice \"principale\"**, quindi, avendo più core, posso eseguire il codice \"principale\" e queste funzioni separate **contemporaneamente**. \n\nQuesta **NON è la soluzione finale**, ma prima di spiegare la prossima ottimizzazione, vorrei dare un'occhiata al **compito** di ciascun **thread**\n\n```rust\nlet (tx, rx) = channel(); \n\nthread::spawn(move || {\n\t// Questa parte l'abbiamo già vista prima 😁\n\tlet (translation, duration) = translate(\n\t\t\u0026read(source_folder.join(\u0026file))\n\t\t\t.unwrap()\n\t\t\t.iter()\n\t\t\t.map(|byte| match byte {\n\t\t\t\t10 =\u003e '\\n',\n\t\t\t\t32 =\u003e 'P',\n\t\t\t\t43 =\u003e '#',\n\t\t\t\t45 =\u003e 'b',\n\t\t\t\tbyte =\u003e (byte + 17) as char,\n\t\t\t})\n\t\t\t.collect(),\n\t);\n\n\t// Controllo se qualche altro thread ha creato il percorso di destinazione di questa canzone\n\tlet target_file = target_folder.join(\u0026file);\n\tlet target_path = target_file.parent().unwrap();\n\tif !target_path.exists() {\n\t\t// Se non dovesse esistere, lo creo io\n\t\tcreate_dir_all(target_path).unwrap();\n\t}\n\n\t// Salvo la traduzione nel file di destinazione\n\twrite(\n\t\ttarget_file.with_file_name(\u0026title).with_extension(\".txt\"),\n\t\ttranslation,\n\t)\n\t.unwrap(); // Controllo eventuali errori\n\n\t// Invio al codice \"principale\" il titolo della canzone tradotta e la sua durata!\n\ttx.send((title, duration)).unwrap();\n});\n```\n\nOgni thread si occupa di leggere il file, convertirne i byte, contare e salvare le note, creare la cartella di destinazione se non esiste, salvare la traduzione e **inviare al codice principale titolo e durata della canzone di cui si è occupato**.\n\nQuest'ultima parte è interessante perché in Rust si fa con i `channel()`, [canali di comunicazione](https://doc.rust-lang.org/book/ch16-02-message-passing.html). Nella prima riga, `tx` è il trasmettitore, e `rx` il ricevitore _(che verrà usato dal codice principale per raccogliere insieme tutti i risultati dei vari thread)_. Di fatto, il thread usa `tx.send()` per invare il dato.\n\n\u003e La sintassi `move || {}` dentro a `thread::spawn()` non fa altro che creare quella che in Python è una **lambda** _(sono simili, ma non la stessa cosa, dato che `|| {}` si comporta come una funzione normale)_\n\n### ...c'è un prezzo da pagare...\n\nEbbene, creare **600 thread** non è gratis. Di fatto il costo è così **alto** su Linux che la versione con i thread ci impiega lo stesso tempo della versione senza thread sul `test03`. La soluzione è facile: basta creare un numero fisso di thread, e distribuire le varie canzoni fra questi thread. Facendo qualche test si scopre che da un certo punto in poi la velocità inizia a peggiorare (12 thread per la mia architettura). \n\nOra vi posso presentare la soluzione finale\n\n```rust\nstatic POOL_SIZE: usize = 12;\n\npub fn umkansanize(source_folder: \u0026Path, target_folder: \u0026Path) -\u003e HashMap\u003cString, i32\u003e {\n    let (tx, rx) = channel();\n\n    scope(|scope| {\n        let mut channels = vec![];\n\n        for _ in 0..POOL_SIZE {\n            let (song_tx, songs_rx) = channel();\n            let tx = tx.to_owned();\n            channels.push(song_tx.to_owned());\n\n            scope.spawn(move || {\n                for (title, file) in songs_rx.iter() {\n                    let (translation, duration) = translate(\n                        \u0026read(source_folder.join(\u0026file))\n                            .unwrap()\n                            .iter()\n                            .map(|byte| match byte {\n                                10 =\u003e '\\n',\n                                32 =\u003e 'P',\n                                43 =\u003e '#',\n                                45 =\u003e 'b',\n                                byte =\u003e (byte + 17) as char,\n                            })\n                            .collect(),\n                    );\n\n                    let target_file = target_folder.join(\u0026file);\n                    let target_path = target_file.parent().unwrap();\n                    if !target_path.exists() {\n                        create_dir_all(target_path).unwrap();\n                    }\n\n                    write(\n                        target_file.with_file_name(\u0026title).with_extension(\".txt\"),\n                        translation,\n                    )\n                    .unwrap();\n\n                    tx.send((title, duration)).unwrap();\n                }\n            });\n        }\n\n        read_to_string(source_folder.join(\"index.txt\"))\n            .unwrap()\n            .split('\\n')\n            .filter_map(|line| {\n                line.strip_prefix('\"')\n                    .and_then(|s| s.strip_suffix('\"').and_then(|s| s.split_once(\"\\\" \\\"\")))\n            })\n            .enumerate()\n            .for_each(|(index, (title, path))| {\n                channels[index % POOL_SIZE]\n                    .send((title.to_owned(), path.to_owned()))\n                    .unwrap()\n            });\n    });\n\n    drop(tx);\n    let mut songs: Vec\u003c_\u003e = rx.iter().collect();\n    songs.sort_unstable_by_key(|(title, duration)| (-duration, title.to_owned()));\n\n    let mut s = String::new();\n    for (title, duration) in songs.iter() {\n        writeln!(s, \"\\\"{title}\\\" {duration}\").unwrap();\n    }\n    write(target_folder.join(\"index.txt\"), s).unwrap();\n\n    songs.iter().map(ToOwned::to_owned).collect()\n}\n```\n\nOra conviene analizzare un pezzo alla volta... _(ometto la funzione `translate()` avendola già discussa) \n\n```rust \nscope(|scope| {\n    let mut channels = vec![];\n})\n```\n\n`scope()` è una funzione estremamente utile, perché fa in modo che **tutti i thread finiscano l'esecuzione** prima di poter procedere. In questo caso, devo aspettare che tutte le canzoni vengano analizzate prima di ordinarle e salvarle nell'`index.txt` della `target_folder`. L'altra cosa utile in questa riga è la **lista** `channels`, che sono una serie di **12 trasmettitori** che userò per **distribuire le 600 canzoni ai 12 thread**\n\nIl comportamento dei thread è simile, se non per questo `for` dato che ogni thread deve analizzare più di una canzone.\n\n```rust\nscope.spawn(|| { for (title, file) in songs_rx.iter() { } ... })\n```\n\n### La fine dell'inizio...\n\nOra possiamo finalmente vedere come viene letto il file `index.txt`, e analizzato per ricavare **titolo** e **file** di ogni canzone\n\n```rust\nread_to_string(source_folder.join(\"index.txt\"))\n\t.unwrap() // Leggo il file in una stringa\n\t.split('\\n') // Divido le righe\n\t.filter_map(|line| { \n\t\tline.strip_prefix('\"')\n\t\t\t.and_then(|s| s.strip_suffix('\"').and_then(|s| s.split_once(\"\\\" \\\"\")))\n\t})\n\t.enumerate() // Stessa cosa di Python \n\t.for_each(|(index, (title, path))| {\n\t\t/* \n\t\t\tDistribuisco titoli e file delle canzoni ai vari thread\n\t\t\tUsando l'operatore %\n\t\t*/\n\t\tchannels[index % POOL_SIZE]\n\t\t\t.send((title.to_owned(), path.to_owned()))\n\t\t\t.unwrap()\n\t});\n```\n\u003e Se non sai cos'è l'operatore `%` (modulo) [qui c'è una spiegazione](https://stackoverflow.com/questions/17524673/understanding-the-modulus-operator)\n\nIn particolare, dentro a `.filter_map()` trasformo ciascuna riga in una tupla e prendo solo i valori che sono validi.\n\n| Trasformazione | Operatore |\n|--|--|\n| \"The daily dinner misspells imagination.\" \"17.txt\" | \n| The daily dinner misspells imagination.\" \"17.txt\" | `.strip_prefix('\"')`\n| The daily dinner misspells imagination.\" \"17.txt | `.strip_suffix('\"')`\n| (The daily dinner misspells imagination., 17.txt) | `.split_once(\"\\\" \\\"\")`\n\nCi sono modi molto più puliti per fare questa cosa, ma questo era il più  veloce. Comunque, SI, Rust è talmente figo che posso leggere un file, elaborarlo e inviare i dati a thread tutto in una sola riga, e comunque scrivere codice pulito.\n\n```rust\n// Colleziono i risultati dei thread in una \"lista\"\nlet mut songs: Vec\u003c_\u003e = rx.iter().collect(); \n\n// Ordino la lista\nsongs.sort_unstable_by_key(|(title, duration)| (-duration, title.to_owned()));\n\n// Converto la lista in una stringa\nlet mut s = String::new();\nfor (title, duration) in songs.iter() {\n\twriteln!(s, \"\\\"{title}\\\" {duration}\").unwrap();\n}\n\n// Salvo la stringa nel file index.txt di destinazione\nwrite(target_folder.join(\"index.txt\"), s).unwrap();\n\n// Converto la lista in un dizionario e ritorno il dizionario\nsongs.iter().map(ToOwned::to_owned).collect()\n```\n\nFinalmente abbiamo raggiunto l'ultimo blocco! E ci sono solo un paio di cose divertenti da spiegare: `sort_unstable_by_key` non è **unstable** perché sta per esplodere 💣, ma perché non garantisce l'ordine relativo... Mi spiego meglio:\n\nSe voglio ordinare la lista `['Banana', 'Abete', 'Alce', 'Carciofo']` solo usando la prima lettera di ogni parola, un sort **stable** **garantisce** che l'**ordine relativo** di due elementi uguali sia **mantenuto** (`'Abete'` e `'Alce'` hanno chiavi uguali siccome iniziano entrambe con `'A'`), quindi `['Abete', 'Alce', 'Banana', 'Carciofo']` è l'unico ordine valido. Se l'algoritmo è **unstable**, anche `['Alce', 'Abete', 'Banana', 'Carciofo']` è un ordinamento valido, visto che `'Alce'` e `'Abete'` iniziano entrambe con la `'A'`, e l'algoritmo non garantisce che l'ordine relativo sia mantenuto _(sono tutte cose che si vedono nel corso di algoritmi)._ In questo caso ha senso usare un algoritmo di ordinamento **unstable** dato che non capita che due canzoni abbiano lo stesso titolo (visto che nel dizionario le chiavi dovrebbero essere tutte diverse) ed è leggermente più veloce rispetto a quello **stable** nel caso medio.\n\n\n```rust\nsongs.iter().map(ToOwned::to_owned).collect()\n```\n\nL'ultima cosa interessante _(e poi vi lascio in pace)_ è quel **ToOwned::to_owned**: in linguaggi come **C**, **C++** e **Rust** si ha a che fare direttamente con la memoria _(tutta roba che Python vi nasconde praticamente)_, e in **Rust** la memoria può appartenere ad una sola variabile alla volta. Quando la funzione `umkansanize()` termina, la memoria della \"lista\" `songs` viene eliminata. Quindi, per poter ritornare il dizionario, devo dire che la memoria di `songs` adesso è di proprietà della funzione che ha chiamato la mia funzione! (un impiccio, ve?) Questo paradigma si chiama **Ownership**, ed è stato introdotto proprio da Rust per evitare tutti quelli che sono problemi di memoria quando si lavora con linguaggi \"vicini alla macchina\". \n\n## In conclusione\n\nSpero che la lettura di questo paragafo vi sia piaciuta e abbiate imparato qualcosa di nuovo (per lo meno avete visto un linguaggio diverso che conoscono in pochi). Mi sono divertito abbastanza con questo homework. Avrei potuto trascrivere molte più informazioni, ma vi avrei solo annoiato e ci ho già dedicato troppo tempo _(fuggo a studiare per i 4 esami che devo dare...)._ Se siete arrivati fino a qui, **buona fortuna per gli esami!**\n\n# FIN\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcuriousci%2Fkeras-library","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcuriousci%2Fkeras-library","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcuriousci%2Fkeras-library/lists"}