Ormai è da qualche mese che ho deciso di elevare F# a mio linguaggio dell'anno e gli darò sicuramente spazio su questo blog nel prossimo futuro, tuttavia nell'ultimo periodo sono stato distratto da Nemerle, un linguaggio compilato a tipizzazione statica costruito anch'esso sulla piattaforma .NET e che unisce contemporaneamente caratteristiche tipiche dei linguaggi funzionali, imperativi e orientati agli oggetti. Nonostante a parole F# e Nemerle possano sembrare linguaggi equivalenti, nella realtà tra i due vi sono differenze sostanziali tanto che quest'ultimo ha:
- un approccio maggiormente OO-centrico
- un sistema di meta-programmazione particolarmente potente attraverso l'utilizzo di macro à la Lisp che lo rende particolarmente estendibile
In effetti la meta-programmazione è un elemento chiave di Nemerle tanto che il core stesso vi si basa pesantemente, vi basti pensare che costrutti base del linguaggio come if, for, foreach e while sono definiti in realtà attraverso delle macro:
macro @if (cond, e1, e2)
syntax ("if", "(", cond, ")", e1, Optional (";"), "else", e2) {
<[
match ($cond) {
| true => $e1
| _ => $e2
}
]>
}
Dal codice è possibile notare come la macro if sia implementata utilizzando internamente una caratteristica tipica di molti linguaggi funzionali definita pattern matching. Curiosamente l'applicazione del pattern matching in Nemerle può essere sfruttata direttamente in altri costrutti come foreach omettendo la keyword match:
1 def range = $[0 .. 5];
2
3 foreach (num in range) {
4 | num when num % 2 == 0 => print($"$num: even | ")
5 | _ => print($"$num: odd | ")
6 }
7
8 // OUTPUT:
9 // 0: even | 1: odd | 2: even | 3: odd | 4: even | 5: odd |
- riga 1: in questa riga istanziamo un range, ma è da notare che esistono anche le list comprehension tipiche di Python (anche se con una sintassi un po' più criptica).
- riga 4: il primo pattern prevede che il numero sia pari, se il valore non combacia con il pattern allora si passa a quello successivo.
- riga 5: l'utilizzo di _ nel pattern matching equivale a un catch all (una sorta di clausola default in uno statement switch) in cui vi si entra se non si verifica alcun match con i pattern specificati in precedenza.
Dall'esempio si può notare inoltre come non sia stato specificato alcuni tipo per gli oggetti, questo grazie al sistema di type inference che permette al compilatore di dedurre i tipi in fase di compilazione. Anche il costrutto try ... catch ... finally è stato leggermente rivisto rispetto al solito per uniformarsi alla filosofia del linguaggio:
def (a, b) = (12, 0);
try {
def n = a / b;
}
catch {
| e is DivideByZeroException => print($"You got to have some guts to divide $a by $b!");
| e is Exception => {
// ...
}
}
// OUTPUT:
// You got to have some guts to divide 12 by 0!
E' evidente quindi come il pattern matching possa essere sfruttato in svariati ambiti che non si limitano a semplici stringhe o numeri, infatti può essere applicato anche nel controllo di tipi (attraverso is, come nell'esempio di try ... catch), su enum o variants (un altro tipo che arriva direttamente dal mondo funzionale), su tuple e liste, su proprietà o campi di un'istanza di classe. Un'altra applicazione particolare del pattern matching si può trovare nelle espressioni regolari:
foreach (str in ["ciao", "aiuola", "casa", "44 gatti"]) {
regexp match (str) {
| "^a.*a$" => printf("\"%s\" inizia e termina con la lettera \"a\"\n", str);
| @"^(?<num : int>\d+).+" => printf("\"%s\" inizia con il numero %d\n", str, num);
| _ => printf("Nessun match per \"%s\"\n", str);
}
}
/* OUTPUT:
Nessun match per "ciao"
"aiuola" inizia e termina con la lettera "a"
Nessun match per "casa"
"44 gatti" inizia con il numero 44
*/
Come accennato in precedenza esiste un tipo Variant (simile al tipo datatype di ML, a cui Nemerle in parte si ispira) il quale, almeno a prima vista, potrebbe sembrare ridondante con il tipo Enum derivato da C# in quanto apparentemente simili ma che in realtà risulta essere molto più potente e flessibile. Un esempio, preso dal sito di Nemerle ma leggermente espanso, vale più di mille parole:
1 variant RgbColor {
2 | Red
3 | Yellow
4 | Green
5 | Different {
6 red : float;
7 green : float;
8 blue : float;
9 }
10 }
11
12 def colors = [ RgbColor.Red,
13 RgbColor.Green,
14 RgbColor.Different(0, 127, 255),
15 RgbColor.Different(blue = 255, red = 0, green = 127) ];
16
17
18 def string_of_color(color : RgbColor) : string {
19 | RgbColor.Red => "red"
20 | RgbColor.Yellow => "yellow"
21 | RgbColor.Green => "green"
22 | RgbColor.Different(r, g, b) => $"rgb($r, $g, $b)"
23 }
24
25 foreach (color in colors) printf("%s\n", string_of_color(color));
26
27 /* OUTPUT:
28 red
29 green
30 rgb(0, 127, 255)
31 rgb(0, 127, 255)
32 */
Possiamo notare alcuni particolari:
- riga 5: il tipo variant permette di creare nuove opzioni partendo da informazioni aggiuntive, in questo caso un colore non contemplato dalle opzioni del tipo variant RgbColor può essere ugualmente espresso sotto forma di valori RGB
- riga 14-15: il costruttore per l'opzione RgbColor.Different può essere invocato passando ad esso i valori degli argomenti con lo stesso ordine della definizione dell'opzione stessa oppure sfruttando i named parameter supportati da Nemerle
- riga 18-19: come nel caso precedente di foreach, possiamo omettere match anche nello sviluppo di funzioni se esso si trova all'inizio del corpo della funzione
- riga 22: possiamo effettuare il binding di variabili ai campi di un'opzione di un tipo variant nello stesso modo in cui abbiamo invocato il costruttore dell'opzione stessa, ovvero seguendo l'ordine della definizione dei campi (come nell'esempio) oppure sfruttando i named parameters, con cui avremmo potuto scrivere:
| RgbColor.Different(red = r, blue = b, green = g) => $"rgb($r, $g, $b)"
Questi sono solo alcuni esempi di programmazione in Nemerle per cominciare a comprendere la filosofia del linguaggio, ne seguiranno ulteriori in altri messaggi ma prima ci tengo a esprimere alcune considerazioni. Nemerle rimane un progetto portato avanti da un gruppo di sviluppatori dell'università di Wrocław senza fondi o supporto esterni, al contrario di F# che è nato in seno al progetto Microsoft Research ed diventato recentemente parte integrante della piattaforma .NET. Questo significa che, nonostante la solidità del design del linguaggio e la sua implementazione generalmente stabile, non è esente da problemi o da qualche mancanza sparsa (personalmente una delle maggiori, anche se by-design, risiede nel fatto che non è possibile implementare iteratori in funzioni locali usando yield). Inoltre all'apparenza il progetto può sembrare poco supportato dai suoi sviluppatori e al limite dell'abbandono, ma in realtà viene portato avanti e attualmente uno degli obiettivi è il rilascio ogni 1~3 mesi di nuove CTP dell'attuale versione instabile 0.9.4. Alla fine dei conti si tratta comunque di un linguaggio abbastanza interessante per certe sue caratteristiche quindi vale la pena giocarci un po'. Tra l'altro è possibile usare Nemerle anche in Linux dal momento che si integra molto bene con Mono, non per niente tutte le mie prove sono state eseguite su una Xubuntu 8.10 con Nemerle 0.9.3 e Mono 1.2.6 (come da repository ufficiali di Hardy Heron) usando SciTE come editor, anche se si tratta di un linguaggio supportato da MonoDevelop.