Julia



Stringhe e caratteri          

In realtà il paragrafo avrebbe dovuto essere intitolato forse "caratteri e stringhe". Questo perchè se è ben vero che le stringhe sono argomento principe in quasi tutti i linguaggi di programmazione va anche detto che esse sono costituite da un insieme di caratteri  più o meno ampio, diciamo da 1 a n con n grande a piacere. I caratteri in se stessi sono sovente un po' bistrattati in alcuni linguaggi, tant'è che in alcuni casi non esiste nemmeno un "tipo carattere" che è in pratica sostituito dalle stringhe di lunghezza uno. In questo linguaggio invece i caratteri sono, come suol dirsi, cittadini di prima classe e, per l'appunto, ad essi è dedicata una classe di nome char che è, in buona sostanza, un intero 32 bit in grado di rappresentare caratteri ASCII classici (per noi occidentali) e Unicode; relativamente a quest'ultima codifica Julia offre un supporto completo.

Cominciamo quindi a parlare proprio dei caratteri.
Da un punto di vista tecnico essi, come detto, sono rappresentati come numeri definiti su una lunghezza di 32 bit e il cui valore numerico è interpretato tramite un codifica speciale. Formalmente invece un carattere è contenuto all'interno di una coppia di doppia apici.

x = 'a'

l'istruzione typeof(x) restituisce semplicemente Char, ovvero la classe dei caratteri. La corrispondenza tra numeri e la loro corrispondeza con i caratteri può essere semplicemente esprimibile tramite le due funzioni char e int che provvedono la conversione nei due sensi:

julia> char(48)
'0'

julia> int('a')
97


Non tutti i numeri possono essere convertiti con successo in caratteri e se volete testare in anticipo questa possibilità potete farlo  usando is_valid_char(n)

julia> is_valid_char(48)
true

julia> is_valid_char(11111111)
false


Naturalmente Julia accetta anche la classica codifica Unicode:

julia> x = '\u48'
'H'

julia> x = '\u49'
'I'


La natura numerica dei caratteri si può evincere anche dal seguente esempio:

julia> x = 'X'
'X'

julia> x + 1
'Y'

Tuttavia è possibile operare anche altre operazioni normali sugli interi, come si può dedurre dalla implementazione interna di Julia stesso:

# numeric operations
+(x::Char , y::Char ) = int(x)+int(y)
*(x::Char , y::Char ) = int(x)*int(y)
div(x::Char , y::Char ) = div(int(x),int(y))


# ordinal operations
+(x::Char , y::Integer) = char(int(x)+int(y))
+(x::Integer, y::Char ) = y+x
-(x::Char , y::Char ) = int(x)-int(y)
-(x::Char , y::Integer) = char(int(x)-int(y))


# bitwise operations
(~)(x::Char) = char(~uint32(x))
(&)(x::Char, y::Char) = char(uint32(x) & uint32(y))
(|)(x::Char, y::Char) = char(uint32(x) | uint32(y))
($)(x::Char, y::Char) = char(uint32(x) $ uint32(y))


bswap(x::Char) = char(bswap(uint32(x)))


<<(x::Char, y::Int32) = uint32(x) << y
>>(x::Char, y::Int32) = uint32(x) >>> y
>>>(x::Char, y::Int32) = uint32(x) >>> y


< (x::Char, y::Char) = uint32(x) < uint32(y)
<=(x::Char, y::Char) = uint32(x) <= uint32(y)

quindi possiamo anche usare il classico operatore di moltiplicazione, di divisione ecc.... potete sbizzarrirvi negli esperimenti che più vi aggradano, ivi compresa ad esempio la somma di caratteri. Come detto questa classe è importante quanto le altre, in Julia. Segnalo anche il metodo sizeof utile per capire quanto spazio si prende il carattere analizzato.

E' il momento ora di parlare delle stringhe. le quali, ve ne accorgerete, sono molto riccamente supportate in questo linguaggio. Tralasciando di sottolineare ancora una volta perchè, come e quanto esse siano importanti nel campo della programmazione, iniziamo dicendo che, anche in Julia, esse sono niente altro che sequenze finite di caratteri (da 1 a n con n grande, praticamente, a piacere) ciascuno dei quali è individuabile tramite un indice. Nessuna novità. Piuttosto raro invece è l'approccio che prevede che il primo indice di una stringa sia 1 invece del più classico 0. Se ricordo bene anche nel caro vecchio Pascal è così. Quindi, il primo carattere di una stringa ha indice 1, il secondo 2 e così via sino arrivare all'ultimo che avrà indice n dove n è il numero di elementi della stringa. Le stringhe sono delimitate da una coppia di doppi apici. Usare l'indice 0 dà origine ad un errore, così come accade manipolando la stringa con indici maggiori del range reale della stessa.

"abc"
"ciao"
"w la Juve"
"1234"


sono esempi validi di stringhe. Vediamo ora un esempio di base:

  Esempio 3.1
1
2
3
4
5
6
s1 = "abcdefg"
println(typeof(s1))
println(length(s1))
println(s1[1])
println(s1[end])
println(endof(s1))

Con il seguente output:

ASCIIString
7
a
g
7

Queste istruzioni non dovrebbero costituire un problema in termini di comprensione:
la riga 1 vede la definizione della nostra stringa
la riga 2 ci presenta il solito typeof, generico, utile più che altro per vedere l'output: non il solito "string" ma qualcosa di più preciso
la riga 3 ci presenta il metodo length, abbastanza comune in tanti linguaggi, che restituisce la lunghezza della stringa, ovvero il numero degli elemento che la compongono
la riga 4 e la 5 vedono l'applicazione di [] per ricavare un singolo elemento della stringa. Nella 4 in particolare usiamo valore end che restituisce l'ultimo elemento della stringa
la riga 8 infine ci presenta il metodo endof per lo più coincidente con length.

La voce "ASCIIString" non è casuale, ovvero non deve essere sottovalutato il suo contenuto informativo. Julia può rappresentare una stringa in vari modi e il compilatore sceglierà sempre quello più economico. L'immagine seguente ci presenta la gerarchia di classi che determinano la natura delle stringhe in Julia. In giallo le classi astratte (delle quali ovviamente parleremo)


UFT8String, ASCIIString ed il suo equivalente esteso UTF32String sono ovviamente le classi di uso comune. Bisogna fare attenzione al fatto che, come si evince dall'immagine qui sopra, UT8String non discende direttamente da DirectIndexString e questo comporta il fatto che le istanze di UTF8String non sono indicizzabili. Ovvero, in parole povere, le stringhe che contengono caratteri che forzino l'uso di UTF8String invece di ASCIIString o UTF32String non sono indicizzabili. La ragione consiste nel fatto che quando indicizziamo lo facciamo per byte, non per carattere, scrivere stringa[n] significa "dammi il byte n-esimo della stringa". La rappresentazione interna in stringhe di tipo UTF8String è ambigua in questo senso, quindi, se avete qualche dubbio sulla stringa che state manipolando, interrogatela sempre con typeof. Nel caso doveste lavorare con i singoli elementi di una stringa di tipo UTF8String dovrete convertirla in un tipo indicizzabile più capinete (e, naturalmente, più pensate in menoria):

StringaUTF32 = UTF32String("stringa-UTF8")


Il nostro operatore [] può anche essere adoperato per estrarre parti di una stringa; per indicare un range la sintassi è molto semplice:
[indice iniziale : indice finale]

julia> s1 = "abcdef"
"abcdef"

julia> s2 = s1[3:3]
"c"

julia> s2 = s1[3:5]
"cde"


E' interessante notare il secondo comando è confrontarlo con l'output di:

julia> s1[3]
'c'

come si vede la valutazione di un range restituisce sempre una stringa mentre applicando [] con un singolo indice si ricava un carattere. Se si vuole ricavare una sottostringa costituita di un solo elemento bisogna fare come nell'esempio presente poco sopra,, indicando un range di tipo [x : x]

La lunghezza di una stringa, tipico problema quando si ha a che fare con queste strutture, si ricava con lenght(stringa):

lenght("aaa")

restituisce il valore 3.

Julia permette l'interpolazione che viene resa possibile dall'uso del simbolo $:

julia> a1 = "stringa1"
"stringa1"

julia> a2 = "stringa2"
"stringa2"

julia> a3 = "stringa3"
"stringa3"

julia> a4 = "$a3$a2$a1"
"stringa3stringa2stringa1"


A questo proposito, è comparso su qualche forum un piccolo problema relativo alle variabili i cui nomi contengono segni di interpunzione, in particolare il punto esclamativo, come visto nel priomo paragrafo Julia non impedisce che tale simbolo compaia nel nome di una variabile. Si veda il seguente esempio: 

julia> zz = 9
9

julia> print("zz vale proprio $zz!")
ERROR: UndefVarError: zz! not defined

julia> print("zz vale proprio $(zz)!")
zz vale proprio 9!


In pratica, basta mettere tra parentesi la variabile per non mandare in confusione il compilatore.

Se vogliamo invece concatenare due stringhe o più ricordiamoci che Julia non permette l'uso del + come invece fanno molti altri linguaggi. Se provate a scrivere cose tipo

c = "a" + "b" ne ricaverete un messaggio d'errore piuttosto chiaro:

ERROR: no method +(ASCIIString,ASCIIString)

invece va bene scrivere la giustapposizione nel caso di stringhe esplicite:

julia> str = "ciao""mondo""attaccato"
"ciaomondoattaccato"


senza però frapporre spazi fra le varie stringhe componenti.

Normalmente però le stringhe sono contenute in variabili di tipo stringa; se le volete concatenare potete usare string o *, come segue:

julia> a = "aaa"
"aaa"

julia> b = "bbb"
"bbb"

julia> c = "ccc"
"ccc"

julia> d = string(a,b,c)
"aaabbbccc"

julia> e = *(a,b,c)
"aaabbbccc"


oppure anche

julia> e = a * b * c
"aaabbbccc"


La funzione string fa anche altre cose. Interessante la capacità di convertire altri tipi di variabili, cosa che * non fa:

julia> a = "1 + 1 = "
"1 + 1 = "

julia> b = a * 2
ERROR: MethodError: `*` has no method matching *(::ASCIIString, ::Int64)
Closest candidates are:
*(::Any, ::Any, ::Any, ::Any...)
*{T<:Number}(::Bool, ::T<:Number)
*(::Complex{Bool}, ::Real)
...

julia> b = string(a, 2)
"1 + 1 = 2"

In questo caso il numero 2 viene da string in formato stringa e questo vale anche per altri oggetti.

Iterare una stringa per un numero di volte a scelta fa uso dell'operatore di elevamento a potenza ^.

julia> x = "ciao "
"ciao "

julia> y = x ^ 3
"ciao ciao ciao "


Cercare uno specifico carattere, o meglio il suo eventuale indice all'interno della stringa, è molto semplice grazie a search. Si tratta in realtà di una potente funzione capace di molti impieghi e che vedremo in azione anche altrove. 

julia> s1 = "abcedfg"
"abcedfg"
julia> print (search(s1, 'b'))
2
julia> print(search(s1, 'z'))
0

search ci restituisce l'indice del carattere cercato oppure, come nell'ultimo caso, 0 se questo non viene trovato. L'indice che viene restituito è quello della prima occorrenza del carattere cercato. E' possibile anche specificare un offset iniziale per la ricerca:

  Esempio 3.2
1
2
3
4
s1 = "abcabcabc"
println(search(s1,'b'))
println(search(s1,'b',6))
println(search(s1,'b',5))

L'output è il seguente:

2
8
5

che dimostra tra l'altro, vedasi l'ultima riga, che l'indice iniziale che abbiamo indicato come indice di partenza è compreso.
Questo vale se cerchiamo un carattere. Se cerchiamo una sottostringa, invece, ci viene restituito un range:

julia> ciao = "Ciao, come va?"
"Ciao, come va?"
julia> print(search(ciao, "iao"))
2:4


Se la sottostringa cercata non viene trovata il risultato è il range [0:-1]

beginswith e endswith sono utili per verificare se la stringa inizia o finisce col carattere indicato:

julia> s = "abcd"
"abcd"

julia> endswith(s,'c')
false

julia> beginswith(s,'a')
true

Per copiare una stringa in un'altra possiamo usare = ovvero l'operatore di assegnazione, La nuova stringa sarà indipendente dalla prima:

julia> s1 = "abcd"
"abcd"

julia> s2 = s1
"abcd"

julia> s1 = string(s1,"w")
"abcdw"

julia> s2
"abcd"


Come si può dedurre dal terzo esempio e poi dal quarto la modifica che ha interessato s1 trasformandola da "abcd" in "abcdw" non ha sortito effetti su s2 che è ancora "abcd".

Rimpiazzare una parte di stringa è possibile tramite replace. La sintassi base è:

replace(stringa, sottostringa-da-sostituire, nuova-stringa)

julia> x1 = "aabbccdd"
"aabbccdd"

julia> replace(x1, "ab", "ww")
"awwbccdd"


Altre due interessanti ed utili istruzioni sono chop e chomp. Approfittiamo di essere per accennare alla possibilità di effettuare operazioni di lettura da una fonte di input. Fondamentalmente questa operazione avviene definendo uno stream sul quale effettuarel a lettura. In questo capitolo ci accontenteremo della fonte più semplice, lo standard input definito tramite la variabile globale STDIN. Il seguente breve esempio chiarisce l'uso di base:

Esempio 3.3
1
2
3
s1 = readline(STDIN)
print(s1)
print(typeof(s1))

La riga 1 effettua l'operazione di lettura dallo standard input ed assegna il risultato di tale lettura alla variabile s1. Si usa l'istruzione readline.
La riga 2 stampa quanto digitato
La riga 3 ci conferma che s1 è una stringa; quindi ciò che ricaviamo dalla lettura della variabile STDIN è quindi, ribadiamolo, una stringa.

Ci soffermeremo ancora più avanti su questo fatto; ora, come entrano in gioco chop e chomp? Non è difficile notare che la stringa in cui riversiamo il risultato della lettura si porta dietro anche i caratteri di "a capo". Le due istruzioni permettono di ripulire la stringa da questa terminazione. In realtà c'è differenza tra le due:

- chop rimuove genericamente l'ultimo carattere
- chomp rimuove specificatamente solo il newline.

Vi starete chiedendo forse come fare se vi si chiede di inserire in input un numero e magari poi fare l'inverso, ovvero convertire un numero in formato stringa e viceversa. Julia vi offre dei metodi molto semplici anche mnmonicamente parlando: int e string.

julia> int("123")
123

julia> string(123)
"123"


Esistono poi le consuete utilities, per esempio lowercase, uppercase per rendere una stringa composta tutta da maiuscole o minuscole, per esempio:

julia> uppercase("abcd")
"ABCD"


e anche  i consueti, in quanto comuni in altri linguaggi, strip, lstrip e rstrip che eliminano gli eventuali spazi bianchi presenti rispettivamente da entrambi i lati della stringa, solo quelli a sinistra e solo quelli a destra.

julia> f = " abc "
" abc "

julia> strip(f)
"abc"


Se volete invertire una stringa usate reverse.

reverse("abc")
 

per ricavare cba come output.

Una funzione curiosa che non ricordo in altri linguaggi (forse anche perchè non l'ho mai cercata) è randstring(n) che genera una stringa casuale composta da numeri lettere maiuscole e lettere minuscole della lunghezza indicata dal parametro n. Con una sola istruzione avrete implementato un generatore di password!

julia> randstring(4)
"Q34l"
julia> randstring(4)
"SEkJ"
julia> randstring(10)
"icGoNaKirr"


Il capitolo sulle stringhe per ora lo chiudiamo qui ma c'è ancora molto altro da dire, come ad esempio tutto il discorso sulle espressioni regolari. Ci torneremo sopra....