Archivio autore: robitex

I magnifici iteratori di #rustlang

È molto che non scrivo sul blog, del resto un blog non è periodico nelle pubblicazioni.
Questa volta i protagonisti del post sono gli iteratori di Rust, il nuovo linguaggio di Mozilla. Mi hanno impressionato fin da subito sia per l’efficacia sia per la facilità dell’implementazione personalizzata.

Non posso per ovvi motivi spiegarvi tutto dall’inizio ne paventarvi il dietro le quinte, quindi questo è solo una sollecitazione per voi internauti impavidi di approfondire l’argomento.

Iterare

Un iteratore è un modo di esplorare una collezione di dati. Esso riduce la complessità dell’insieme alla manegevolezza astratta del singolo elemento.

Numeri pari

Se volessimo stampare i numeri pari 0 a 24 potremo far ricorso alla funzione di libreria range_step_inclusive() che fornisce un opportuno iteratore che, come si può notare nel codice seguente, rimane nascosto dallo zucchero sintattico del costrutto for:

for i in range_step_inclusive(0u, 24, 2) {
    println!("{}", i);
}

Le magie degli iteratori in Rust cominciano qui:
possiamo applicare un filtro che trasforma un iteratore in un altro:

for i in range_inclusive(0u, 24)
    .filter(|n| n%2 == 0) {
    println!("{}", i);
}

Oppure possiamo trasformare l’iteratore applicando una funzione in un altro iteratore, sempre per ottenere i numeri pari:

for i in range(0u, 13).map(|n| n*2) {
    println!("{}", i);
}

Ok. Stringhe e numeri

Come passare da una stringa come questa “0,2,4,6,8,10” al vettore [0, 2, 4, 6, 8, 10]?

Nella libreria standard esiste la funzione split() che fornisce l’iteratore sulle parti della stringa individuate tramite un carattere separatore.
Ma questo iteratore fornisce stringhe, così possiamo applicare una mappatura per tradurre la stringa in numeri.
Ma un iteratore di numeri non è ancora il vettore desiderato. Poco male, basta passarlo alla funzione collect() ed il gioco è fatto!

Ecco il codice che, con alcuni dettagli che possiamo trascurare per il momento, attua i tre passaggi principali detti:

let v = "0,2,4,6,8,10"
    .split(',')
    .map(|s| from_str::<uint>(s).unwrap())
    .collect::Vec<uint>()

println!("{}", v);

Possiamo eseguire ‘nel cloud’ il codice con questo link su Playpen.

Finale

Fantastico mondo quello degli iteratori di Rust!
In realtà non ho mai ancora detto cosa è effettivamente un iteratore in Rust. È un peccato perché vi accorgereste dell’eleganza del panorama guardando dalla vetta della montagna, ma vi avevo avvisati, néh…

Alla prossima ma senza promessa e con questa premessa.
🙂
R.

Numeri in lettere in Rust

Da numero a lettere

Tradurre un numero in lettere è semplice con una funzione ricorsiva. Implementeremo lo stesso algoritmo già scritto in Java, in Python, in Lua ed in Go, in Rust, il nuovo linguaggio di Mozilla Foundation ancora in fase di sviluppo.

Il problema (ormai classico per me…)

Dato un numero intero scrivere una funzione che restituisca la sua rappresentazione in lettere nella lingua italiana.
Per esempio il numero 1842 dovrà essere convertito nella stringa ‘milleottocentoquarantadue’.

Soluzione in Rust, due osservazioni

Il nome scelto per la funzione di conversione è num2text() ed il codice per la versione 0.10pre di Rust è riportato di seguito, ma prima vorrei evidenziare un paio di cose.
La prima osservazione è la più semplice: come potete notare la funzione num2text() non contempla alcuna istruzione return. Infatti in Rust tutto è espressione!
Come si conviene a qualunque espressione la sua valutazione ritorna sempre un valore che possiamo assegnare ad una variabile oppure — in una funzione — intendere come il valore stesso da restituire.

Quando il valore prodotto dall’espressione non è utile, il modo previsto dalla semantica di Rust per eliminarlo è semplicemente annullarlo inserendo il carattere ; finale (punto e virgola). A prima vista un listato in Rust assomiglia ad un listato in C od in C++ a causa della presenza di questi simboli finali, ma non si tratta di separatori sintattici (no, non sono necessari), ma bensì del modo di specificare che il valore dell’espressione non deve essere utilizzato, o più precisamente che il valore da restituire è il tipo unità, una tupla vuota. Ed anche questo è importante perché in Rust non esiste il tipo nullo.

Nel codice seguente il costrutto match restituirà il valore della funzione come il risultato dell’espressione che compare nel ramo individuato dall’intervallo in cui è compreso il numero da tradurre, perché non c’é il putno e virgola finale. Ed è interessante a questo proposito leggere il codice del ramo 200..999 dalla riga 34, dove vengono concatenate tre stringhe di cui quella centrale è costruita addirittura un ciclo if…

Ciclo if che mi ricorda gli albori dell’informatica, quando Ada Lovelace introdusse in un articolo scritto intorno al 1842 l’istruzione di salto condizionale, probabilmente per la prima volta nella storia, ad alimentare l’idea alla base dei linguaggi di programmazione.

La seconda cosa importante riguarda il tipo da restituire: il numero convertito è certamente una stringa ma occorre stabilire dove memorizzare il dato, ovvero se costruire un valore tipo boxed oppure un riferimento unboxed.

Il codice e niente altro

fn main() {
	println!("{} -> {}", 21, num2text(21));
	println!("{} -> {}", 24, num2text(24));
	println!("{} -> {}", 28, num2text(28));
	println!("{} -> {}", 624, num2text(624));
	println!("{} -> {}", 154578, num2text(154578));
	println!("{} -> {}", 45987000, num2text(45987000));
	println!("{} -> {}", 98630120, num2text(98630120));
}

// array per definizione nomi da 1 a 19
static a19: [&'static str, ..20] = [
	"", "uno", "due", "tre", "quattro","cinque", "sei", "sette", "otto",
	"nove", "dieci", "undici", "dodici", "tredici", "quattordici",
	"quindici", "sedici", "diciassette", "diciotto", "diciannove"];

static dec: [&'static str, ..8] = ["venti", "trenta", "quaranta",
    "cinquanta", "sessanta", "settanta", "ottanta", "novanta"];

fn num2text(n: uint) -> ~str {
	match n {
		0..19  => a19[n].to_owned(),
		20..99 => {
			let d = dec[n/10-2];
			match n % 10 {// truncate the last char
				1 | 8 => d.slice_chars(0, d.char_len() - 1),
				_ => d
			}.to_owned() + num2text(n % 10)
		}
		100 .. 199 => "cento" + num2text(n%100),
		200 .. 999 => {
			a19[n/100].to_owned()
			+ if (n%100)/10 != 8 { "cento" } else {"cent"}
			+ num2text(n%100)
		}
		1_000 .. 1_999 => "mille" + num2text(n%1000),
		2_000 .. 999_999 => {
			num2text(n/1000)
			+ "mila"
			+ num2text(n%1000)
		}
		1_000_000 .. 1_999_999 => "unmilione" + num2text(n%1_000_000),
		2_000_000 .. 999_999_999 => {
			num2text(n/1_000_000)
			+ "milioni"
			+ num2text(n%1_000_000)
		}
		1_000_000_000 .. 1_999_999_999 =>
			"unmiliardo" + num2text(n%1_000_000_000),
		
		_ => {
			num2text(n/1_000_000_000)
			+ "miliardi"
			+ num2text(n%1_000_000_000)
		}
	}
}

Conclusioni

Rust si sta evolvendo rapidamente. Introduce elementi che chiarificano alcuni essenziali concetti che in C od in C++ restavano impliciti e nascosti nel compilatore, in particolare nella gestione della memoria.

Spero che possa trovare diffusione come desidera la comunità dei suoi sviluppatori.
Saluti.
R.

Stringhe in Rust

Stringhe

In Rust il tipo stringa è rappresentato da un vettore di byte (cioé di tipo u8 unsigned integer ad 8 bit) nella codifica Unicode UTF-8.
Per le stringhe l’uso della memoria è quindi identico a quello dei vettori e possiamo riferirci al post precedente per saperne di più sull’argomento.

In questo post presenterò alcuni ulteriori esempi di codice sulle stringhe, rispetto al post precedente introduttivo.

Concatenare stringhe

L’operatore + concatena stringhe di qualsiasi tipo ma restituisce sempre una stringa ~str quindi un unique vector, tipo che può cambiare dinamicamente dimensione. Ecco un esempio con un po’ tutte le combinazioni possibili tra stringhe &str e ~str:

fn main() {
    let x = ~"abcd";
    let y = "123";
    assert_eq!(~"Message: " + x, ~"Message: abcd");
    assert_eq!("Message: " + x, ~"Message: abcd");
    assert_eq!("Message: " + y, ~"Message: 123");
    assert_eq!("Message: " + y.to_owned(), ~"Message: 123");
    assert_eq!( x + y, ~"abcd123");
}

Stringhe palindromi

Una stringa è palindroma se leggendo da destra e da sinistra un carattere alla volta si incontrano sempre caratteri uguali.

Volendo scrivere una funzione che verifichi se una stringa è palindroma o no, possiamo ricevere un riferimento &str da considerare un dato in sola lettura.
Un primo modo di implementare questa funzione è quello di fare un test sul vettore ottenuto dalla stringa argomento con indici che dagli estremi si incontrano al centro:

fn pal(s: &str) -> bool {
    let v_chars: ~[char] = s.chars().collect();
    let n = v_chars.len();

    for i in range(0, n/2) {
        if v_chars[i] != v_chars[n - i - 1] {
            return false;
        }
    }
    return true;
}

Un altro modo è quello di prelevare dalla stringa iniziale due slice parte destra e parte sinistra, e usufruire dell’iteratore normale per la prima e l’iteratore reverso per la seconda per confrontare ancora un carattere alla volta:

fn pal_2(s: &str) -> bool {
    let n = s.char_len();
    let s1 = s.slice_chars(0, n/2);
    let s2 = s.slice_chars(n-n/2, n);
    let mut i_r = s2.chars_rev();

    for c1 in s1.chars() {
        if c1 != i_r.next().unwrap() {
            return false;
        }
    }
    return true;
}

Gli iteratori e le funzioni relative della libreria standard di Rust li trovate a questo link.

A ben vedere è possibile costruire un iteratore che restituisce le coppie di caratteri da confrontare, tramite l’iteratore zip(), ecco come:

fn pal_z(s: &str) -> bool {
	for (dx, sx) in s.chars().zip(s.chars_rev()) {
		if dx != sx {
			return false;
		}
	}
	return true;
}

Davvero interessante! L’iteratore restituisce la tupla (&dx, &sx) ed il ciclo for pensa a destrutturare con il pattern matching il dato.
A ben vedere questa soluzione elegante andrebbe applicata a metà stringa altrimenti gli iteratori si superano ed inutilmente continuano a confrontare i caratteri fino alla scansione doppia della stringa.

Reversare stringhe

Con un codice in stile classico potremo fare così:

fn reflect(s : &str) -> ~str {
    let mut rf = ~"";
    rf.reserve(s.len());
    for chr in s.chars_rev() {
        rf.push_char(chr);
    }
    return rf;
}

ma con uno più idiomatico potremo semplicemente scrivere:

fn reflect_2(s : &str) -> ~str {
    s.chars_rev().collect()
}

Ricordarsi che in Rust ogni cosa è un’espressione: se non inseriamo il punto e virgola finale il risultato viene restituito e può essere utilizzato come valore di ritorno senza che sia necessario un return.

Farfugliare

Cercando algoritmi di esempio sul web ho trovato questo simpatico esercizio: mescolare le vocali con dei caratteri ‘f’ così che il testo ‘ramo’ diventi ‘rafamofo’.
Eccone l’implementazione in Rust che fa uso del nuovissimo contenitore StrBuf aggiunto di recente alla libreria standard completamente riorganizzata della versione 0.10:

fn farf(s : &str) -> ~str {
    let mut res = std::strbuf::StrBuf::new();
    for c in s.chars() {
        match c {
            'a' | 'e' | 'i' | 'o' | 'u' => {
                    res.push_char(c);
                    res.push_char('f');
                    res.push_char(c);
              }
            _ => res.push_char(c)
        }
    }
    return res.into_owned();
}

Conclusioni

A mano a mano che si utilizzano i costrutti del linguaggio diviene naturale ricercare uno stile più idiomatico sperimentando codice più elegante e compatto.
La libreria standard di Rust è perfettamente in sintonia con i principi di solidità del linguaggio, per primo quello dell’assenza del tipo nullo.

Negli esercizi con le stringhe abbiamo avuto modo di utilizzare ampiamente gli iteratori, per un codice pulito ed efficiente.

Alla ‘profossifimafa’!
R.

Rust, vectors and string

Vettori

Oggi parliamo di vettori in Rust, che definiamo come:
un vettore è un segmento di memoria contiguo in cui sono memorizzati un certo numero di dati dello stesso tipo

vector_image

Poiché i dati sono contigui la loro posizione è indicizzabile tramite l’operatore [] da 0 a n-1, con n numero degli elementi del vettore stesso.
In linea con la filosofia di Rust, accedere a dati con indice fuori intervallo dovrebbe produrre un errore di compilazione — come accade in Go — invece attualmente (con la versione 0.9pre) produce un errore a runtime come dichiara il manuale, forse per motivi di performance.

In altri linguaggi, e mi viene in mente casualmente il Go, il vettore è chiamato con il termine array, e ciò può aiutare a rendere il concetto più famigliare.
Quello che non abbiamo ancora detto sui vettori è se la sua dimensione può cambiare oppure no. In Rust esistono tutte e due le possibilità. Cominciamo dalla seconda…

Vettori fixed-sized

I vettori a lunghezza fissa vengono istanziati nello stack come valori unboxed.
Significa che essi vengono eliminati dalla memoria in automatico quando termina il blocco in cui sono stati definiti, e che la copia è la costruzione di un nuovo vettore e non quella di un nuovo riferimento allo stesso vettore.
Gli elementi ereditano la mutabilità o l’immutabilità del vettore essendo il loro proprietario (leggere la puntata precedente per approfondimenti).

Se T è il tipo di dato degli elementi allora il tipo del vettore a lunghezza fissa si scriverà come [T, ..n], ed n dovrà essere un intero positivo letterale o quanto meno dal valore determinabile a tempo di compilazione (valore statico). La dimensione fa quindi parte del tipo.

Fino ad ora non ci sono differenze sostanziali con il Go, ma ecco finalmente il primo esempio di codice:

fn main() {
    // vettore a lunghezza fissa
    let v = [10, 20, 30];
    assert!(v[0] == 10);
    assert!(v[2] == 30);

    // altra espressione letterale di vettore
    // mutevole con 10 elementi di valore `true`
    let mut q = [true, ..10];
    assert!(q[0] == true);
    q[0] = false;
    assert!(q[0] == false);

    // ...con dichiarazione esplicita del tipo
    let s: [f64, ..3] = [1.0, 2.0, 3.0];
    println!("s = [{}, {}, {}]",
        s[0], s[1], s[2]); // stampa `s = [1, 2, 3]`

    // il primo termine determina il tipo dell'elemento:
    let t = [1.0_f64, 2.0, 3.0]
    let z = t[3] // runtime error: index out of bounds
}

Esempi minimi con gli unboxed vectors

Somma elementi

Scrivere una funzione che calcola la somma degli elementi di un vettore di interi di lunghezza 10.
La prima versione legge ogni valore tramite l’operatore di indicizzazione []:

fn sum_1(v: [int, ..10]) -> int {
    let mut s = 0i;
    for i in range(0, 10) {
        s += v[i];
    };
    return s;
}

Ma si può far ricorso alle funzioni della libreria standard implementate per l’oggetto vettore, per esempio iter() che fornisce un iteratore su tutti gli elementi tramite borrowed pointer e quindi di tipo &T (in questo caso &int):

fn sum_2(v: [int, ..10]) -> int {
    let mut s = 0i;
    for &value in v.iter() {
        s += value;
    };
    return s;
}

L’ultima versione parla in Rust più di tutte (riparleremo delle espressioni lambda e degli iteratori della libreria standard (forse)):

fn sum_3(v: [int, ..10]) -> int {
    let mut s = 0i;
    v.map(|val| s += *val);
    return s;
}

Determinante di una matrice 2×2

Una matrice a due dimensioni può essere implementata come un vettore di vettori:

fn main() {
    // matrice 2x2
    let m: [[int, ..2], ..2] = [[1, 2], [-3, 4]];
    // calcolo del determinante
    let detm = m[0][0] * m[1][1] - m[0][1] * m[1][0];
    println!("det M = {}", detm);
}

I vettori possono essere instanziati anche con il costruttore breve con cui si ottiene un vettore di elementi tutti uguali (come avevamo già visto in un esempio precendente in cui abbiamo istanziato un vettore di 10 booleani):

fn main() {// costruttore breve...
    let uv: [int, ..3] = [10, ..3];
    println!("{:?}", uv); // stampa `[10, 10, 10]`
}

Unique vectors

Ah ecco. Siamo giunti al cuore dell’argomento: gli ‘unique vectors’.
Si tratta di vettori a lunghezza variabile — a lunghezza indefinita nella terminologia di Rust — implementati come valori owned boxed. Questi dati occupano quindi la memoria dinamica e vengono eliminati quando il puntatore unico ad essi esce dal blocco in cui è definito.
La copia della variabile trasferisce alla nuova la ‘proprietà’ del vettore mentre quella originaria non può più essere usata (move semantics).

Se T è il tipo degli elementi, allora ~[T] è il tipo del puntatore proprietario. Abbiamo alla fine giustificato l’aggettivo ‘unique’ per questo tipo, notando che ad esso non è stato assegnato il nome di ‘owned vector’ forse perché farebbe pensare ad un vettore a lunghezza fissa nell’heap, mentre il nuovo nome ‘unique vector’ evidenzia la particolarità della lunghezza variabile.

Un esempio:

fn main() {// aggiungiamo due elementi...
    let mut uv: ~[int] = ~[1, 2, 3];
    uv.push(40);
    uv.push(50);

    println!("{:?}", uv); // stampa `~[1, 2, 3, 40, 50]`
}

Slice

Lo slice è una vista su un segmento continuo di un vettore, costituito da un puntatore al vettore e da una lunghezza che non fa parte del tipo. Il suo tipo è indicato con &[T] dove T è il tipo degli elementi.
Lo slice non è il proprietario degli elementi ma se mutabile può modificarli.

Uno slice si può ottenere direttamente con la forma letterale (vector expression), oppure lo si può derivare da un vettore, oppure ancora con la funzione slice() della libreria standard, sia in modo immutabile che mutabile:

fn main() {
    // slice da forma letterale
    let s1 = &[1, 2, 3];

    // slice da altro vettore
    let s2: &[int] = [4, 5, 6];

    // slice dalla funzione `slice()`
    let v = [7, 8, 9];
    let s3 = v.slice(1, 3); // v.slice(a, b) -> [a, b)

    println!("{:?}", s1); // stampa: `&[1, 2, 3]`
    println!("{:?}", s2); // stampa: `&[4, 5, 6]`
    println!("{:?}", s3); // stampa: `&[8, 9]`

    // slice mutevole da forma letterale
    let s4 = &mut [10, 11, 12];
    s4[0] += 1_000; // l'underscore è ignorato

    // slice mutevole da altro vettore
    let s5: &mut[int] = [13, 14, 15];
    s5[0] += 1_000;

    // slice mutevole da funzione
    let mut w = v;
    let s6 = w.mut_slice(0, 2);
    s6[0] += 1_000;

    println!("{:?}", s4); // stampa `&mut [1010, 11, 12]`
    println!("{:?}", s5); // stampa `&mut [1013, 14, 15]`
    println!("{:?}", s6); // stampa `&mut [1007, 8]`
}

Eliminare i doppioni dal vettore

Come esempio interessante vogliamo scrivere una funzione che dato un vettore restituisca un secondo vettore con gli elementi del primo ma senza duplicati.
La funzione può ricevere il vettore convenientemente come slice e restituire un unique vector:

fn clean_copies(v: &[int]) -> ~[int] {
    let mut z: ~[int] = ~[];
    for &elem in v.iter() {
        let mut is_uni = true;
        for &u in z.iter() {
            if elem == u {
                is_uni = false;
                break;
            }
        }
        if is_uni {
            z.push(elem);
        }
    }
    z
}

La funzione è già abbastanza efficiente perché il ciclo interno, non appena viene trovato che l’elemento è un duplicato, si interrompe, ma può essere ancora più veloce specie con vettori molto grandi, utilizzando una mappa.
Quindi, già questo semplice problema ci da due ulteriori argomenti da sviluppare: implementare la funzione con una mappa e generalizzarla a tutti i tipi, numerici o stringa, che possono essere confrontati…

Stringhe

E veniamo alle stringhe. In Rust le stringhe sono tipi primitivi di nome ‘str’ ma sorprendentemente non sono tipi istanziabili direttamente: sono permessi solo i tipi reference a ‘str’. Avremo cioé il tipo stringa dinamico ‘owned’ ~str gestito da un puntatore proprietario e il tipo ‘managed’ @str gestito dal garbage collector che libererà la memoria dalla stringa quando non esisteranno più puntatori ad essa, e infine il tipo slice &str.

Le stringhe sono rappresentate internamente come vettori di interi u8 (unsigned byte) nella codifica UTF-8. Dunque a differenza di quel che accade in molti altri linguaggi, in Rust le stringhe possono essere mutabili visto che possono esserlo i vettori.
Quando sono espresse in forma letterale ‘non decorata’, cioé senza i prefissi ~ o @, avremo solamente stringhe immutabili — del tipo &str — la cui vita si estende fino alla fine del programma (static lifetime).

A dimostrazione che le stringhe sono vettori, proviamo a stampare un elemento alla volta di una stringa sapendo che un carattere può essere rappresentato in UTF-8 in più byte:
fn main() {
// a is a ‘unique string’ (possiamo dire)
let a = ~”è sera”;
for i in range(0, a.len()) {
println!(“a[{}] = ‘{}'”, i, a[i]);
}
}

Mettiamo anche alla prova la mutabilità delle stringhe. Per questo non possiamo usare una stringa letterale perché immutabile per definizione.

fn main() {
    let mut s = ~"";
    s.push_char('P');
    s.push_char('i');
    s.push_char('p');
    s.push_char('p');
    s.push_char('o');
    s.push_str('...');
    // stampa risultato
    println(s);
}

Controlliamo anche la concatenazione si stringhe di diverso tipo:

fn main() {
    let s1 = "immutabile e borrowed"; // tipo &'static str
    let s2 = ~" più owned";           // tipo ~str
    let s3 = @" più managed";         // tipo @str

    println(s1+s2+s3);

    let a: ~str = s1+s2+s3;
    println(a);
    let b: &str = s1+s2+s3;
    println!("{}", b);
}

Tutto a posto. L’operatore di concatenazione ‘+’ è capace di gestire insieme stringhe di diversi riferimenti, mentre il compilatore è in grado di tradurre il risultato della concatenazione nel tipo specificato, come accade nelle ultime due righe di codice.

Nota finale

Secondo me, la prima cosa da studiare in Rust è il modello della memoria e quindi i tipi unboxed e boxed, che ho cercato di spiegare nel post precedente a questo, citato all’inizio. Possiamo poi seguire strade diverse ma penso che l’argomento logico successivo sia proprio questo: i vettori e di conseguenza anche le stringhe essendo un particolare tipo di vettore.

Quello che ho scritto dovrà essere ricontrollato con le prossime versioni di Rust che oggi è ancora in fase di sviluppo con una documentazione spesso ridotta all’osso (comunque il codice presentato compila correttamente con la versione 0.9pre di Rust).

Sia i vettori che le stringhe sono implementate secondo i concetti generali di gestione della memoria in Rust. Peccato che il compilatore non esegua il controllo sui valori degli indici quando questi assumono un valore statico, ma penso che ci sia una buona ragione perché non sia così.

Saluti.
R.

Rust 0.9

Ciao a tutti,
vi segnalo che è uscita la versione 0.9 di Rust.
Sembra sia stato fatto un notevole lavoro rispetto alla precedente 0.8 e, soprattutto, ci avvicianiamo finalmente alla versione stabile 1.0.
Chissà, forse, finalmente, ho trovato il linguaggio che non cambierò per un bel pezzo… Rust
R.

Piccolo aggiornamento (13 gennaio).
Sul blog Rust ‘n Stuffs è apparso The State of Rust 0.9 con un po’ di dettagli sulla release. Impressionanti i benchmark.

Ma, realisticamente il post si conclude con:
Is Rust Ready Yet?
Nope. It still has some work to do. 1.0 is estimated before the end of 2014, though that may slip depending on how things land. […]

OK, Roberto, io e –credo– tanti altri attendiamo fiduciosi 😀

Rust variables and pointers

Memory model

pointerdiagram

Rust, il nascente linguaggio di Mozilla Foundation, mette a disposizione ogni sorta di modi per memorizzare oggetti ma sempre in modo sicuro. La sicurezza consiste nel fatto che gli errori di programmazione, per esempio dimenticarsi di liberare la memoria relativa ad un oggetto non più necessario, viene intercettata in fase di compilazione.

Questo articoletto è una sorta di traduzione di questo post di Patrick Walton che ha il pregio di essere concreto nelle spiegazioni e quindi di far capire cosa significano in pratica i tanti concetti che nei manuali ufficiali di Rust costituiscono una nuova terminologia.

Giusto per fare un’autocitazione 🙂 potete studiarvi il tema delle variabili e dei puntatori leggendo questo post dal titolo “La varietà di variabili in Go”, peraltro solo un assaggio in confronto alla molteplicità di puntatori che troviamo in Rust…

Il tutorial ed il manuale di riferimento del linguaggio sono letture “tecnicamente colte”, ma proprio per questo lasciano spaesati i neofiti che non dispongono di una robusta conoscenza in scienza dell’informazione.

E forse, e dico forse, una volta compresi i concetti “rugginosi” la scrittura del codice in Rust diventa divertente.

Tipi

Un tipo è intuitivamente una categoria di oggetti con proprie operazioni. Un numero intero, una stringa di testo, un array, una funzione, sono chiaramente tipi diversi.

A proposito, in Rust abbiamo un controllo totale sui tipi: possiamo utilizzare le capacità di inferenza locale del compilatore che desume il tipo in automatico (come alla riga 2 dell’esempio seguente), i suffissi (come alla riga 4), oppure le dichiarazioni esplicite del tipo (riga 3).

Il linguaggio Rust è a tipi statici, che significa che tutti, e dico tutti, gli oggetti devono essere di un tipo e che questo tipo deve essere univocamente determinato già in fase di compilazione (ecco il motivo dell’aggettivo ‘statici’).

Unboxed value

Le variabili valore sono tipi unboxed — e qui cominciano subito i termini rugginosi — occupano cioé uno slot di memoria nello stack, che è una sorta di catasta di oggetti molto veloce usata per i dati locali.

Quando termina una funzione, tutte le variabili unboxed locali cessano di essere utili — ovvero escono dallo ‘scope’. La memoria di lavoro nello stack viene rapidamente liberata in automatico, solo per questo fatto.
In altre parole, i dati unboxed sono informazioni la cui vita è regolata dal contesto in cui sono stati creati. Quando il contesto termina di esistere, anche gli oggetti che contiene vengono eliminati. Il fatto che un simbolo sia utilizzato nel codice del programma dentro o fuori dal suo contesto può essere controllato dal compilatore, senza aggravio a runtime.

La copia della variabile comporta la creazione di un nuovo slot indipendente poiché la variabile è la diretta rappresentazione del valore nello stack:

fn main() {
    let mut a = 200;    // nuova variabile intera `a`
    let b: int = a;     // creazione copia `b`
    a = 100i;           // riassegnazione diretta del valore
    // risultato
    println!("a={}, b={}", a, b);    // stampa: a=100, b=200
}

Questo vale anche per oggetti più complessi come le strutture:

// copia di strutture...
fn main() {
    struct Point {x: f64, y: f64};
    let mut A = Point{x: 0.0, y: 0.0};
    let B = A;    // B copia indipendente di A
    A.x = -1.0;
    A.y = -1.0;
    // risultato
    println!("A=({}, {})", A.x, A.y);    // stampa: A=(-1, -1)
    println!("B=({}, {})", B.x, B.y);    // stampa: B=(0, 0)
}

Da notare che rendere mutevole la variabile A implica che anche i suoi campi lo saranno. Nella terminologia di Rust, questo si esprime dicendo che le proprietà dei componenti ereditano quelle della variabile, ma non è sempre così come vedremo dopo con i valori boxed, se la variabile non è il “proprietario” dell’oggetto.

Rust ed i boxed values

Un box è un’area nell’heap che, a differenza dello stack, è una memoria che ha una gestione dinamica: una volta creato il valore assume una vita indipendente dal contesto.
In Rust, per scelta di progetto, la memoria deve essere liberata in automatico senza che lo sviluppatore se ne debba occupare esplicitamente ma allora nasce il problema di come distruggere un dato nella memoria dinamica quando esso non è più utile: ci sono varie soluzioni che esamineremo subito.

Owned boxes

Una prima soluzione automatica è far si che il boxed value appartenga alla variabile puntatore che gestisce il suo riferimento, così che quando essa uscirà di scopo verrà eseguita la funzione distruttore del box.
Ci sono quindi due oggetti: il primo è il boxed value che occupa un’area della memoria dinamica, diciamo di tipo T, il secondo è il riferimento ‘owner’ del box che si trova memorizzato nella memoria di lavoro dello stack, di tipo ~T.

In Rust, per creare un “puntatore unico proprietario” del dato nella memoria dinamica, si utilizza il segno tilde ~:

{ // un blocco come questo è un contesto
    let x = ~123;        // x è il puntatore di tipo ~int
    println!("{}", *x);  // deferenziare il puntatore per il valore
} // il contesto termina
// qui `x` diviene `out of scope`
// il distruttore libera la memoria dinamica
// poi anche x viene eliminato dallo stack

Sembrerebbe che questo tipo di puntatori proprietari non sia utile, di più, sembrerebbe che usare un owned box sia solo un aggravio di elaborazione visto che lo stack è più veloce e la vita del box nell’heap è legata a quella del puntatore. Tanto vale usare un valore unboxed…
In effetti è così. Tuttavia per implementare una lista od un albero con strutture ricorsive i puntatori owned sono indispensabili perché in Rust gli oggetti sono rappresentati in memoria direttamente ed una definizione ricorsiva produce un tipo di grandezza infinita, mentre con un puntatore no.

Move semantics

Questa volta, copiare un “owned box” come abbiamo fatto per le variabili nello stack, significa passaggio di proprietà (può esistere un solo puntatore per ogni owned box altrimenti cadrebbe la stessa qualità di proprietario):

fn main() {
    let x = ~100;       // x è di tipo ~int
    let y = x;          // y è ora il nuovo proprietario del box
                        // da qui in poi x non si può più usare!!
    println!("{}", *x); // compile error: use of moved value: `x`
}

La move semantics può essere utilizzata per far diventare il box mutevole o, viceversa, immutabile e, ancora una volta, il compilatore controlla che non siano utilizzati ex-proprietari.

Managed boxes

Una seconda soluzione automatica di gestione della memoria dinamica è quella di eliminare ciò che non serve più tramite il garbage collector.

Un ‘managed box’ è un valore nella memoria dinamica, diciamo di tipo T, la cui vita (lifetime) è gestita dietro le quinte dal garbage collector dal momento in cui non esistono più riferimenti ad esso, di tipo @T (‘oggetto ‘managed’ si crea con il simbolo @).
Ha differenza dell’owned box, un managed box non ha nessun proprietario. Di conseguenza possono essere creati più di un puntatore ad un solo box:

fn main() {
    let x = @mut 123; // tipo @int, puntatore a managed box
    let y = x;        // ancora tipo @int
    *x = 321;
    println!("x={}, y={}", *x, *y); // stampa: x=321, y=321
    // altro reference allo stesso box
    let z = @mut y;
    *z = 456;
    println!("x={}, y={}, z={}", *x, *y, *z); // stampa: x=456, y=456, z=456
} // adesso il managed box viene eliminato dal GC
// non ci sono più riferimenti ad esso

Rimane da capire perché la riga 2 dell’esempio è ‘let x = @mut 123;’ e non ‘let mut x = @123;’. La spiegazione è in realtà semplice: se il box può avere più di un puntatore che lo referenzia allora non solo il valore nel box può essere o meno mutevole, ma anche il puntatore stesso può cambiare assumendo il riferimento ad un altro managed box.
In questo esempio, simile a quello che si trova nel tutorial di Rust, vengono presentati i vari casi:

let a = @100; // variabile e box immutabile:
              // a = @1; non consentito assegnare un nuovo box
              // *a = 1; non consentito cambiare il valore del box

let b = @mut 200; // variabile immutabile ma box mutabile:
                  // b = @1; no
                  // *b = 1; si

let mut c = @300; // variabile mutabile e box no
                  // c = @1; si
                  // *c = 1; no

let mut d = @mut 400; // variabile e box mutabili
                      // d = @1; si
                      // *d = 1; si

// posso assegnare solo oggetti
// di mutabilità compatibile:
c = a; // ok
d = b; // ok
c = b; // no, c accetta solo box immutabili
d = a; // no, a accetta solo box mutabili

Borrowed pointers

La terza soluzione è quella apparentemente più bizzarra: un puntatore ad un oggetto nella memoria dinamica che non fa nulla per liberarla.
Questo contraddice l’idea che in Rust la memoria viene liberata in modo automatico, a meno che il puntatore non sia un reference ad un oggetto dei tre tipi ‘unboxed’, ‘owned boxed’ o ‘managed boxed’.
Infatti, in questo caso la memoria verrà correttamente liberata nei rispettivi modi dell’oggetto, e la distruzione del borrowed pointer non causerà nulla di più.

L’utilità di questo puntatore — che si ottiene con il simbolo & ed è quindi di tipo &T — sta per esempio nel fatto che possiamo riferirci ad un owned box senza violare la regola del puntatore proprietario unico, oppure possiamo scrivere funzioni in cui non è necessario distinguere il tipo di oggetto (unboxed T, owned boxed ~T o managed boxed @T):

// dal tutorial...
struct Point{x: f64, y: f64}

let on_the_stack :  Point =  Point { x: 3.0, y: 4.0 };
let managed_box  : @Point = @Point { x: 5.0, y: 1.0 };
let owned_box    : ~Point = ~Point { x: 7.0, y: 9.0 };

fn compute_distance(p1: &Point, p2: &Point) -> f64 {
    let x_d = p1.x - p2.x;
    let y_d = p1.y - p2.y;
    sqrt(x_d * x_d + y_d * y_d)
}

// ora per esempio...
compute_distance(&on_the_stack, managed_box);
compute_distance(managed_box, owned_box);

Gli oggetti boxed sono passati alla funzione compute_distance() come puntatori dal compilatore, mentre l’operatore . per l’accesso ai campi della struttura, deferenzia in automatico il puntatore.

Riassumendo

Un valore unboxed è un dato in memoria che viene automaticamente distrutto al termine della durata del contesto in cui è stato definito, tipicamente al termine dell’esecuzione di una funzione;

Un valore owned boxed è un dato nella memoria dinamica che viene automaticamente distrutto quando l’unico puntatore proprietario viene eliminato essendo un unboxed type. Il cambio di proprietario è detto ‘move semantics’ e potrà esistere un solo puntatore proprietario alla volta.

Un valore managed boxed è un dato nella memoria dinamica che viene distrutto al momento opportuno dal garbage collector, quando non esistono più reference al box. Possono esserci più reference che puntano allo stesso box come anche un borrowed pointer. A scelta, un reference può essere mutevole o no, ed un box può essere mutevole o no.

Un borrowed pointer è un reference che non influisce sulla lifetime dell’oggetto puntato, non è né proprietario né responsabile della mutabilità del valore ma è solo un semplice reference.

Conclusioni

Rust mette a disposizione diversi modi di gestire la memoria tra cui scegliere essendo certi che essa verrà liberata automaticamente al momento opportuno e potendo contare sul controllo statico a tempo di compilazione. In questo, il linguaggio chiarisce e rende espliciti i concetti della rappresentazione in memoria dei dati al prezzo di una curva di apprendimento più ripida.

Nell’implementare il codice lo sviluppatore può decidere quanto essere efficiente in esecuzione regolando il livello d’astrazione e complessità degli oggetti in memoria, senza temere di introdurre errori difficili da scovare.

Il linguaggio non ha ancora raggiunto la versione stabile 1.0 (oggi si trova alla 0.8 e la 0.9 è in lavorazione), e la comunità pur molto attiva, è composta da un numero ristretto di sviluppatori ed utenti, ma lancia la sfida.

Alla prossima.
R.

Rust: primo contatto

Rust, chi era costui?

The Rust logo

The Rust logo

Avevo già sentito nominare Rust come linguaggio di programmazione di sistema ma un colloquio al recente GuITmeeting a Roma, ha innescato la mia curiosità.

Tornato dallo splendido weekend romano tra passione storia e viaggio, ho consultato il sito ufficiale di Rust e la pagina corrispondente di Wikipedia.

Con questo post mostrerò i miei primi contatti con il linguaggio dopo aver letto il tutorial dal sito.

Installazione

Al momento, per Windows è disponibile un pacchetto pronto per l’installazione scaricabile dal sito ufficiale, ma per Linux o OS X è necessario compilare i sorgenti. L’operazione non è poi troppo difficile ma richiede un calcolatore con una discreta dotazione hardware (almeno 2GB di RAM), altrimenti si rischia di fare cosa “buona per riscaldare la stanza d’inverno” cit.

Per fortuna esistono un paio di repository su Launchpad da cui si possono installare le versioni del compilatore e degli strumenti necessari, adatti alla nostra versione di Ubuntu.
Per esempio quello di Hans Jørgen Hoel più aggiornato o quello di Kevin Cantu.

Io ho scelto quest’ultimo accontentandomi della versione 0.6 perché la mia Ubuntu è Lucid e nel primo repo non è presente la versione dedicata più recente: la 0.8. Ho installato Rust con i seguenti tre comandi:

$ sudo add-apt-repository ppa:kevincantu/rust
$ sudo apt-get update
$ sudo apt-get install rust

Prove

In Lua la keyword per definire una funzione è function, in Go è func ed in Rust è fn! Ecco quindi il classico Hello World!:

fn main() {
    println("Hello world!")
}

Il sorgente inserito in un file di testo va compilato con il comando (presupponendo di chiamare il sorgente ‘hello.rs’):

$ rust build hello.rs

Altro piccolissimo esempio: implementiamo la funzione gradino che restituisce 1 se l’argomento è positivo, 0 se zero e -1 se negativo:

fn main() {
	println(signum(123).to_str());
}

fn signum(x: int) -> int {
	if x < 0 {
 		-1
 	} else if x > 0 {
		 1
	} else {
		 0
	}
}

Perché manca il ‘return’?
In Rust l’istruzione condizionale if come gli altri costrutti del linguaggio, è un’espressione ciò significa che il condizionale produce un valore, che poi è preso come valore di ritorno della funzione.
Il simbolo del punto e virgola ; NON è il terminatore di riga necessario alla sintassi, ma bensì il modo per annullare l’espressione. In altre parole, con il punto e virgola istruiremo Rust a considerare l’espressione come uno statement. Il valore dell’espressione risulterà così non significativo.
Se infatti si inserisce il ; al termine del blocco dell’if (riga 12) si riceve questo errore:

0: 13:1 error: not all control paths return a value

La creazione di una variabile in Rust non è un’espressione, cioè in essa non viene restituito un valore ma viene compiuta un’azione. Ne segue che il ; finale risulta obbligatorio:

// creiamo una variabile intera
let a = 1000; // ok
let b = 2000  // error: expected `;`

Come avrete notato serve la chiave ‘let’ per creare una variabile (con la terminologia di Rust si dovrebbe dire: per creare uno slot), ma non è tutto.
Per default le variabili sono immutabili, ovvero non possono essere riassegnate. Solo aggiungendo il modificatore ‘mut’ la variabile diventa tale. In questo modo si deve esprimere nel codice cosa è destinato a cambiare, evitando errori:

let a = 1000;
let mut b = 2000;

a += 1; // error: re-assignment of immutable variable
b += 1; // ok

Prime impressioni

Rust è un linguaggio di programmazione sviluppato sotto l’egida di Mozilla Foundation che mira a diventare un C/C++ migliore. Per questo si confronta con il Go che viene giudicato in un’intervista allo stesso Autore di Rust, un buon linguaggio.

Il Go può attrarre sviluppatori dal mondo Python/Ruby. Nonostante che Rust disponga di più strutture dati del Go, sembra essere più tecnico, meno semplice da imparare, e forse potenzialmente migliore. Staremo a vedere se finalmente un nuovo linguaggio sostituirà od almeno affiancherà il C, ancora oggi saldamente sul trono dal 1978.

Precisione iii

Corto circuito

Questo post è un bellissimo corto circuito: conosco Orlando perché è un amico del GuIT che tra l’altro ho conosciuto l’anno scorso di persona al GuIT meeting di Napoli. E proprio Orlando ha commentato il post di Juhan Precisione su questo stesso blog, di qualche giorno fa su alcuni calcoli.
Questo dimostra che gli appassionati non vedono l’ora di scoprire nuove cose sul proprio argomento preferito.

Chiudo il circuito

Eccomi dunque a chiudere il circuito informatico presentrando un programma in Go che esegue le stesse operazioni eseguite in awk ed in Fortran:

package main

import (
    "fmt"
    "math/big"
)

func main() {
    s, u := big.NewInt(2), big.NewInt(1)
    for i := 1; i <= 10; i++ {
        // a = s - 1
        a := big.NewInt(0).Set(s).Sub(s, u)
        // s*a + 1 = s*(s - 1) + 1
        s.Mul(a, s).Add(s, u)
        fmt.Println(i, s)
    }
}

Tecnicamente, vorrei farvi notare che è possibile in Go concatenare le chiamate dei metodi come per esempio si legge alla riga 12 del codice sorgente, se il metodo stesso ritorna lo stesso oggetto.

Ho utilizzato il pacchetto dei grandi numeri big necessario perché dopo la sesta iterazione i numeri superano il limite massimo del tipo int64. Certo avrei potuto utilizzare il tipo uint64 ma potevo chiudere il circuito soltanto esagerando alla grande portando il ciclo fino a 10 iterazioni… ecco di seguito il risultato:

1 3
2 7
3 43
4 1807
5 3263443
6 10650056950807
7 113423713055421844361000443
8 12864938683278671740537145998360961546653259485195807
9 165506647324519964198468195444439180017513152706377497841851388766535868639572406808911988131737645185443
10 27392450308603031423410234291674686281194364367580914627947367941608692026226993634332118404582438634929548737283992369758487974306317730580753883429460344956410077034761330476016739454649828385541500213920807

A questo punto, ci sta propio bene un
Alla Prossima!
R.

Il termine dell’appalto

Sommario

Calcoleremo la data di termine di un appalto a partire da quella di inizio lavori dal numero di giorni naturali e consecutivi stabiliti dal contratto, utilizzando due linguaggi di programmazione: Lua e Go.

L’occasione

Potevamo utilizzare un banale foglio di calcolo per determinare la data del termine di fine lavori per un contratto d’appalto? No… No perché ci si annoia sempre a fare le stesse cose e sentiamo la necessità di migliorarci.

Calcolo

La descrizione del calcolo che seguiremo è presto fatta: alla data di inizio sommiamo il numero di giorni stabiliti per completare l’opera e togliamo un giorno, per ottenere così la data cercata corrispondente al termine dei lavori.

Infatti, il giorno della consegna conta già uno ai fini del tempo contrattuale. Possiamo vedere la cosa considerando l’istante di tempo della mezzanotte: è al contempo il primo istante del giorno ma anche l’ultimo di quello precedente.
Tuttavia la mezzanotte è sempre interpretato come il primo istante del giorno successivo. Per la data di inizio è corretto ma non per la data di termine.

Lua

In Lua, utilizzando le funzioni della libreria interna “os”, costruiamo la data di inizio lavori che viene gestita come il numero di secondi trascorsi da un certo momento (il classico epoch Unix). Sommiamo poi il numero di secondi corrispondenti alla durata dell’appalto, sottraendovi un secondo per ottenere l’attimo prima della fine dell’ultimo giorno di lavoro.
La funzione termina restituendo la stringa della data formattata eventualmente come specificato nell’ultimo argomento facoltativo.

-- restituisce la data di scadenza dell'appalto
-- nota la data di consegna ed il numero di
-- giorni naturali e consecutivi della durata
-- dei lavori contrattuali

local function endingDate(d,m,y, days, frmt)
    local secsperday = 24*60*60
    local t_start = os.time{day=d,month=m,year=y,hour=0}
    local t_end   = t_start + days*secsperday -1
    frmt = frmt or "%d/%m/%Y"
    return os.date(frmt, t_end)
end

print(endingDate(26,8,2013, 60))

Go

Anche in Go utilizziamo la libreria disponibile con il linguaggio, in particolare il pacchetto “time”, per restituire nello stesso formato precedente la data risultato.

package main

import (
    "time"
    "fmt"
)

func main() {
    fmt.Println(endingDate(2013,8,26, 60))
}

func endingDate(y int, m time.Month, d, days int) string {
    s := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
    e := s.AddDate(0, 0, days).Add(-time.Second)
    return fmt.Sprintf("%d/%02d/%d",e.Day(), e.Month(), e.Year())
}

Finale

Le differenze con Lua possono sembrare ad un primo sguardo minime, ma non è così anche se entrambi hanno feature in comune come il supporto alla concorrenza, le funzioni di prima classe e le closure.

Io, la prima cosa che ho notato è la maggiore precisione della libreria “time” di Go rispetto a Lua (che si riflette nel modo in cui percepiamo le date), e l’uso trasparente in Go di tipi sinonimi di quelli di base come per esempio nella rappresentazione del mese — il tipo time.Month per intenderci.

Penso però che la differenza sostanziale sia nell’importanza che il linguaggio da ai tipi: in Lua la tipizzazione è dinamica come si conviene per un linguaggio di scripting mentre in Go è statica: nel codice occorre sempre definire il tipo di dato e solo rispetto a questa classificazione i dati possono essere elaborati, per esempio da una funzione).

Il risultato è che in Go si è maggiormente portati a pensare concettualmente ai dati come termini del problema, strutturando il codice. Ovvio che questa differenza è la stessa che troviamo confrontando qualsiasi coppia di linguaggi con tipi dinamici e statici (come Python e Java), ma il Go è certamente uno dei linguaggi a tipi statici che si avvicinano di più ai linguaggi a tipi dinamici…

Un saluto.
R.

Leggere un foglio di calcolo .ods in Go

Sommario

Una breve descrizione di come leggere il contenuto di un foglio di calcolo nel formato Open Document Format di estensione .ods con una piccola libreria in Go.

Obiettivo

Dato un foglio di calcolo di Libre Office od anche di Apache Open Office vorremmo leggere il contenuto della cella “A1” della prima tabella, utilizzando il Go, il linguaggio open source creato da Google.

I prerequisiti per poter essere operativi e tentare la sfida sono: un file nel formato OpenDocument, standard ISO, di un foglio di calcolo naturalmente, ed una installazione di Go per il vostro sistema operativo.

Un piccolo trucco

Sappiamo che il file di estensione .ods è in realtà un file compresso contenente dati xml. Occorre quindi decomprimere il file, leggere all’interno il file relativo ai dati ed interpretare il codice testuale xml.

Ebbene, il piccolo trucco consiste proprio in questo: procurarsi una libreria già pronta che fa tutto questo per noi. Guarda caso una libreria così esiste davvero ed è ospitata su github e messa gentilmente a disposizione per l’uso e lo sviluppo libero da knieriem.
Lo sviluppo comunitario è qualcosa di incredibile anche per il numero elevatissimo di progetti, qualcosa che a pensarci mi sorprende sempre. Grazie davvero.

La libreria odf contiene pochissimi file ed è, al momento, praticamente priva di documentazione, ma può leggere e scrivere fogli di calcolo nel formato aperto della collezione OpenDocument.

Per installarla un semplice comando farà al caso nostro e se va tutto bene ci vuole veramente un attimo:

 $ go get github.com/knieriem/odf

Il codice di lettura del file ods

Bene, adesso tocca a noi!
Diamo al file del foglio elettronico il nome di “test.ods” (questo nome lo ritroveremo nel sorgente) e posizioniamolo in una directory assieme al nostro sorgente che per adesso è ancora un file vuoto.

La prima cosa che dovrà fare il programma, a regola, è aprire il file in lettura. Dunque proviamo questo primo codice in Go (prima di poterlo eseguire naturalmente occorre compilarlo):

package main

import (
	"github.com/knieriem/odf/ods"
	"fmt"
	)

func main() {
	f, err := ods.Open("test.ods")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()
}

Il programma non fa ancora nulla: semplicemente apre il foglio elettronico ed esce, e serve solo per provare che sia tutto a posto e per cominciare, perché no, a prendere confidenza con la sintassi del Go.

Leggere il contenuto della cella

Siamo pronti per chiudere la sfida (a nostro favore ovvio). Il prossimo passo una volta aperto il file è quello di leggerne il contenuto. La libreria odf mette a disposizione la funzione ParseContent() che accetta come parametro il puntatore ad una struct documento e restituisce un eventuale errore.
Per saperne di più sui puntatori basta leggere i precedenti post sull’argomento in particolare questo.

Disponendo ora del documento possiamo accedere tramite alcuni metodi (ancora in corso di esplorazione da parte mia), ai dati del foglio elettronico ed in particolare possiamo osare nell’ordine a fare:

  1. stampare il numero delle tabelle contenute nel file;
  2. stampare il nome del primo foglio di calcolo;
  3. stampare (finalmente) il contenuto della cella A1;

Ecco il listato completo che chiude questa breve esplorazione.

Ma prima un saluto a tutti, ed in particolare a Juhan che non credo possa fare altrettanto con i linguaggi che frequenta lui: uno ha il nome di un temibile serpente, un altro somiglia ad una pietra preziosa di colore rosso, eccetera.
🙂

package main

import (
	"github.com/knieriem/odf/ods"
	"fmt"
	)

func main() {
	f, err := ods.Open("./test.ods")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	// parsing del contenuto del file
	var doc ods.Doc
	if err := f.ParseContent(&doc); err != nil {
		fmt.Println(err)
		return
	}

	// stampa il numero di tabelle
	fmt.Println(len(doc.Table))

	// stampa il nome del primo foglio
	fmt.Println(doc.Table[0].Name)

	// stampa cella "A1"
	firstrow := doc.Table[0].Strings()[0]
	fmt.Println(firstrow[0])
}