Linguaggi e schemi sintattici

Linguaggi e apprendimento

I am sitting in a room different from the one you are in now. I am recording the sound of my speaking voice and I am going to play it back into the room again and again until the resonant frequencies of the room reinforce themselves so that any semblance of my speech, with perhaps the exception of rhythm, is destroyed. What you will hear, then, are the natural resonant frequencies of the room articulated by speech. I regard this activity not so much as a demonstration of a physical fact, but more as a way to smooth out any irregularities my speech might have.   A.Lucier


Imparare a programmare un software e ad usarlo nel comporre un brano musicale è come apprendere una nuova lingua straniera attraverso la quale esprimere le proprie idee. Come abbiamo già in parte appurato nel Paragrafo dedicato a Linguaggi e codici, è qualcosa che ha a che fare con la comprensione e l’apprendimento parallelo di due linguaggi, uno tecnico e l’altro espressivo-comunicativo. Nell’addentrarci in questo ragionamento, proviamo ad affrontare brevemente alcune teorie e tecniche di apprendimento, sia per cercare di illustrare come è strutturato questo sito, sia per comprendere come si svilupperà il cammino appena iniziato. Poniamoci alcune domande.

Quali sono le condizioni generali perché ci sia apprendimento?

Prima di apprendere è necessario comprendere correttamente e per comprendere è necessario interpretare. Per interpretare dobbiamo conoscere il sistema di regole usato (grammatica) e possedere un vocabolario minimo formatosi nella memoria attraverso l’esperienza (Nel Capitolo precedente abbiamo affrontato un discorso simile riguardo la memoria auditiva e sonora). Ad esempio nel leggere queste righe di testo, le interpretiamo e le comprendiamo in quanto conosciamo la sintassi e siamo provvisti di un vocabolario più o meno forbito della lingua italiana. In un secondo tempo potremo scegliere se apprendere o dimenticare tutto ciò che abbiamo letto, interpretato e compreso. La comprensione è sempre un atto di scelta, anzi una serie di scelte compiute lungo lo sviluppo di un’esperienza come ad esempio la lettura di un testo (sia esso letterario, musicale, pittorico, gestuale o altro) e in quanto tale un atto interpretativo [M.Ambel]. Apprendimento è dunque innanzitutto interpretazione, scelta e comprensione. Non c’è apprendimento senza comprensione e non c’è comprensione senza conoscenza pregressa. E’ un processo di stratificazione. La comprensione di un testo non veicola significato in sé, ma offre solo indicazioni a chi legge sul modo di costruire significato partendo da conoscenze acquisite in precedenza. Nello specifico di questo scritto, il solo comprendere sia i contenuti estetico-musicali che quelli tecnico-informatici qui esposti non porta conseguenzialmente a scrivere un brano musicale più o meno udibile o esteticamente interessante utilizzando un linguaggio informatico più o meno performante, ma vuole fornire gli strumenti più adatti per accrescere, connettere tra loro e filtrare le proprie esperienze artistiche e non. E’ il processo successivo alla comprensione e all’apprendimento che veicola significato e permette la creazione, sopratutto nelle arti del suono dove come abbiamo visto nel Capitolo precedente significante e significato coincidono (un suono rappresenta solo se stesso) permettendo in questo modo la libertà della forma. Per le ragioni appena esposte, in questo scritto incontreremo paragrafi-vocabolario come il successivo o quelli sulla misurazione del tempo, apparentemente astratti e senza alcuna applicazione pratica, che contengono quasi esclusivamente descrizioni di elementi appartenenti alternativamente a uno dei due linguaggi che sono oggetto delle nostre riflessioni. Questi paragrafi servono in primo luogo alla formazione delle conoscenze minime necessarie alla comprensione dei due linguaggi (primo stadio del processo di stratificazione dell’apprendimento) e in secondo luogo hanno funzione di reference, ovvero definiscono uno spazio dove è possibile trovare un insieme di informazioni relative ad argomenti dedicati. Potremo scegliere di leggere, comprendere e apprendere (ricordare) subito il contenuto significativo di questi capitoli o meno, ma in entrambi i casi avremo appreso che, qualora servisse in futuro, c’è un luogo dove poter cercare determinate informazioni.

Come avviene la comprensione?

Se prendiamo in considerazione la teoria cognitivista la comprensione è determinata dall’applicazione di strutture che trasformano la realtà circostante in rappresentazioni mentali che si formano attraverso l’elaborazione progressiva nella nostra memoria di schemi flessibili e combinabili tra loro. Questi schemi sono una rappresentazione delle nostre esperienze e assumono un ruolo fondamentale nel processo del comprendere perché guidano la nostra conoscenza e ne consentono lo sviluppo attraverso la trasformazione continua di modelli mentali della realtà. La stessa teoria divide queste strutture in due entità: script e piani. Uno script è una particolare conoscenza schematica che la mente elabora circa "eventi e situazioni nei quali una sequenza di azioni viene eseguita in un determinato contesto spazio-temporale da uno o più attori che agiscono per raggiungere uno scopo e adottano comportamenti idonei alla situazione in cui si trovano”. [Roger Schank, "Teaching Minds: How Cognitive Science Can Save Our Schools" Paperback – October 28, 2011]. Vediamone un esempio.

Questo è lo script che abbiamo seguito per andare al ristorante la prima volta. Si è formato attraverso l’interpretazione e la comprensione della realtà, ovvero ci siamo resi conto di essere in un luogo preciso (casa), di essere nelle condizioni di doverci lavare e vestire (vocabolario sociale pregresso) e di dover aprire la porta di casa per uscire e dirigerci all’indirizzo del ristorante scelto. Dopo aver vissuto la prima volta questa esperienza, ora sappiamo (abbiamo appreso) che se fossimo a casa e volessimo tornare in quel ristorante dovremmo effettuare di nuovo tutte le azioni presenti in quello script. Lo stesso può inoltre essere richiamato dalla memoria e applicato anche nel caso volessimo andare in un altro ristorante o in qualsiasi luogo diverso da casa nostra, basterebbe semplicemente cambiare il percorso da compiere una volta varcato l’uscio. Lo script appena illustrato è rappresentativo della flessibilità degli schemi. Quando nel corso di un esperienza riconosciamo uno schema già incontrato lo colleghiamo immediatamente alla nuova situazione ed eventualmente lo trasformiamo per ottimizzarlo o riadattarlo, generandone uno nuovo che potrà a sua volta essere in futuro rielaborato e modificato interamente o in parte. Sempre R.Schank definisce lo script come ”copione di interazione sociale, rintracciabile nel proprio patrimonio culturale”. Quando è attivo (l’abbiamo appreso) consente di:

Ora che il concetto generale di script dovrebbe risultare più chiaro veniamo a un esempio più specifico dei temi affrontati in questo scritto (musica e codice) che spero possa fornire la chiave per comprendere come utilizzare al meglio gli schemi esemplificativi musicali e informatici presenti in questo sito.

Due considerazioni:

  1. Come è ben osservabile in questo caso anche lo script non veicola significato in sé ma offre solo indicazioni sul modo di costruirlo. Musicalmente parlando, gli eventi di questo script possono essere note, audio files, ampiezze, durate, contenitori di altri eventi, parametri di una qualche tecnica di sintesi o elaborazione del suono, etc. A seconda della tipologia di eventi che andiamo a mettere in sequenza il significato musicale può cambiare radicalmente. Lo schema è sempre lo stesso, similare o derivato. Anche dopo aver stabilito il parametro rappresentato dall’evento e di conseguenza scelto il linguaggio (sonoro, musicale, visuale o altro) che vogliamo utilizzare, nel momento in cui andiamo a modificare ”i criteri secondo i quali le scelte casuali debbano avvenire e come i valori ottenuti saranno impiegati per rendere sequenziali gli eventi nel tempo” il significato può cambiare radicalmente, in quanto muta la sintassi stessa del linguaggio impiegato. Lo schema è ancora una volta lo stesso, similare o derivato.
  2. Lo script appena esposto mostra chiaramente le relazioni e i collegamenti che intercorrono tra il linguaggio musicale e quello informatico: effettuiamo le azioni principali su quest’ultimo (programmiamo algoritmi) ma queste azioni sono subordinate alle necessità del primo (scopi musicali). Anche questo scritto, per quanto possibile è organizzato allo stesso modo. Dall’esposizione di una o più necessità musicali e dal loro apprendimento passeremo alla descrizione di alcune possibili realizzazioni informatiche. Solo in qualche occasione ci soffermeremo su schemi puramente informatici che, in virtù della loro astrazione e delle caratteristiche sopra esposte, possono essere applicati con minime varianti a più contesti linguistico/musicali.

Ricordiamoci che la teoria cognitivista divide gli schemi in script e piani. Abbiamo chiarito cosa sono i primi ma prima di vedere cosa sono questi ultimi, poniamoci un’altra domanda la cui risposta rappresenta il terreno che congiunge il concetto di script con quello di piano.

Quale processo consente la conservazione in memoria delle nuove informazioni?

Nello svolgersi dei ragionamenti effettuati finora in questo paragrafo abbiamo stabilito che l’apprendimento è efficace quando:

Riassumendo, l’apprendimento è efficace quando ci consente di trasferire in nuovi contesti quanto abbiamo appreso. Questo avviene quando le conoscenze e le esperienze si configurano in strutture a grappoli, interconnesse da legami di subordinazione nella nostra memoria. Semplificando. Pensiamo i singoli script come acini di un grappolo d’uva, legati tra loro non da un solo pedicello e ramo ma in più punti e tra diversi grappoli.

image not found

Sono questi grappoli di conoscenze a costituire il contenuto della struttura cognitiva, che nel tempo si accresce e si ristruttura continuamente. Il continuo elaborare, confrontare con la realtà e ristrutturare i grappoli di conoscenza facilita e consente la conservazione in memoria delle nuove informazioni. Tanto più varie e pluridisiplinari saranno le esperienze, tanto maggiore sarà la ricchezza della loro struttura cognitiva e quindi la nostra capacità di metterle in relazione e apprendere.

image not found

Possiamo considerare questo Paragrafo così come altri che incontreremo più avanti, come una rappresentazione ideale del processo di transfert della conoscenza. Non è strettamente correlato né all’informatica, né tantomeno alla musica, infatti qualcuno avrà già pensato ”ma cosa me ne importa di tutto ciò? io faccio il musicista, non lo studioso di comportamenti umani!”. Ci accorgeremo però in futuro, che al momento opportuno (un momento diverso per ognuno di noi), magari nel leggere il giornale o guardando un tramonto, richiameremo alla memoria e collegheremo più o meno volontariamente i concetti espressi in queste righe con quelli letti in un’altra parte di questo scritto. Forse riaffioreranno e si connetteranno con gli stessi concetti anche frasi lette su un cartellone pubblicitario o sentite per radio in coda sull’autostrada. Improvvisamente nascerà nella nostra mente una nuova idea. Questa operazione, che alcuni chiamano ispirazione altro non è che la traduzione in un qualche linguaggio umano del momento in cui per qualche ragione vengono richiamati, si sovrappongono o si raccordano nel nostro vissuto diversi schemi esperienziali intrecciati in reti di grappoli di conoscenza. E ne creano uno nuovo. Possiamo solo tentare di descrivere il meccanismo, non insegnare a replicarlo. Successivamente dobbiamo però dare una forma a questa nuova idea e per farlo dobbiamo stabilire dei piani, delle strategie. Se gli script corrispondono agli schemi che descrivono un percorso da compiere per raggiungere un determinato scopo e sono interconnessi tra loro in strutture a grappolo, i piani sono la ricerca dello schema migliore o grappolo di schemi migliore da percorrere per il raggiungimento dello scopo all’interno del nostro database esperenziale (”fare un piano strategico”). Infine dovrebbe essere ora più chiaro il perchè della citazione del testo di ”I am sitting in a room” di A.Lucier all’inizio del Paragrafo. Questo è il testo che l’esecutore legge durante la performance (clicchiamo sui link per ulteriori notizie sul brano). E’ anche il piano dell’autore e del performer. L’esecutore infatti legge la descrizione di quello sta facendo, dove e come lo sta facendo e quale è lo scopo (musicale) che persegue. In questa situazione paradossa il piano e lo script coincidono con il significato stesso del brano, non sono una veicolazione di significato ma l’essenza del significato stesso. La musica qui non è un semplice ascolto di un linguaggio più o meno codificato e condiviso ma diventa partecipazione condivisa a una nuova esperienza che produce eventi sonori nel tempo.

SuperCollider

Elementi del linguaggio

Tutti gli elementi che scriviamo nel codice di SuperCollider (lettere, parole, numeri, simboli) sono oggetti.

2        // e' un oggetto
3.56     // e' un oggetto
''ciao'' // e' un oggetto
\miao    // e' un oggetto
b        // e' un oggetto
SinOsc   // e' un oggetto
Rand(1,3).postln // sono due oggetti

Ogni singolo oggetto corrisponde a un tipo di dato definito, ovvero viene interpretato dal software in base a una propria identità.

2        // e' un int (numero intero)
3.56     // e' un float (numero decimale)
''ciao'' // e' una stringa
\miao    // e' un simbolo
$b       // e' un char
SinOsc   // e' una UGen
Rand(1,3).postln // 'Rand' e' una Classe con due argomenti, invocata con il metodo '.postln'

Le identità che hanno una rappresentazione sintattica diretta, o meglio gli elementi minimi costitutivi del linguaggio di SuperCollider si chiamano literals. Nella lingua parlata potremmo paragonarli a soggetto, verbo, complemento, preposizione, etc. In italiano ad esempio se vogliamo chiedere a qualcuno di compiere un’azione dobbiamo inanellare questi elementi in una frase:

  soggetto verbo  articolo complemento aggettivo
’’LUIGI    SUONA  UN       DO          PIANO!’’

Per comunicare con SuperCollider è necessario fare la stessa cosa, ovvero chiedergli di compiere delle azioni attraverso un linguaggio a lui comprensibile:

// Crea e suona immediatamente un oscillatore sinusoidale con:
// una frequenza random tra 897 e 1345Hz,
// una fase iniziale a 0
// un'ampiezza random tra 0 e 0.5

{SinOsc.ar(rrand(897,1345),0,rand(0.5))}.play(s);

Come possiamo osservare il linguaggio da utilizzare per comunicare con SuperCollider è decisamente meno prolisso dell’italiano. Per prima cosa vediamo dunque quali sono i Literals principali.

Literals

I neofiti e chi vuole arrivare rapidamente a una realizzazione musicale possono saltare momentaneamente la lettura di questo paragrafo passando direttamente al successivo, o meglio leggerlo velocemente tralasciando la piena comprensione o l’esercizio mnemonico, a patto di tornarci in seguito, quando la pratica informatico-musicale e sintattico-lessicale di SuperCollider sarà più ampia. Come altri parti di questo scritto, può essere utilizzato come reference.

Numeri

I numeri corrispondono a un sistema di notazione posizionale, sono colorati in viola e possono essere rappresentati in diversi modi.

// int
 8;
 -1254;

// float
2.763;
-0.26536;

// notazione esponenziale;
1.5e2; // e2 = 1.5 * 100
1e 4;  // e 3 = 1 *  1000
       // e + numero di 0 dopo l'uno del moltiplicatore

// pi greco:
pi;
2pi;
0.5pi;
0.25pi;

Characters

I caratteri (characters) sono un unità simbolica corrispondente a un grafema (le singole lettere), sono preceduti dal segno $ e sono di colore verde scuro. In SuperCollider come vedremo possiamo usare le lettere singole senza farle precedere da questo simbolo (ad esempio nelle variabili globali), ma se per caso volessimo identificare un tasto sulla tastiera del computer dovremmo utilizzare un character.

Symbols e Strings

Se una parola in SuperCollider è inclusa tra apici o tra virgolette assume tipi di data differenti, nel primo caso diventa un symbol, nel secondo una string.

Backslash

Come in numerosi linguaggi tra i quali Java e C il simbolo \ (backslash) identifica gli escape character. In SuperCollider è usato principalmente in tre modi:

Identifier

I nomi delle variabili e dei metodi cominciano obbligatoriamente con lettere minuscole e si chiamano identifier (identificatori). Devono essere formati da una sola parola e possono contenere il simbolo _ (underscore).

12.postln            // int (12) e metodo (.postln)
(var ciao_ciao_mare) // variabili e underscore

Classi e UGens

I nomi delle Classi e delle UGens cominciano obbligatoriamente con lettere maiuscole e sono di colore blu.

Object
Point
Synth
SinOsc
Pan2

Vedremo in seguito le caratteristiche di questi oggetti in quanto la trattazione di questo argomento richiede ampio spazio.

Valori speciali

I valori speciali che riguardano la matematica booleiana sono scritti come parole (true, false). La parola speciale nil significa vuoto. Sono di colore blu.

a = true   // vero
b = false  // falso
c = nil    // vuoto

Literal array

Vedremo gli array nel dettaglio ed a lungo nei prossimi capitoli per ora pensiamoli come ad un insieme di numeri inclusi tra due parentesi quadre. I literal array sono degli array preceduti dal simbolo # (cancelletto), che ’blocca’ il contenuto dell’array, ovvero se mettiamo un cancelletto davanti a un array non potremo più modificarne il contenuto. A livello computazionale sono molto più veloci da calcolare che non gli array normali.

[1, 2, 'abc', "def", 4]  // array normale: posso modificare
                         // dinamicamente  il contenuto nel corso
                         // della computazione
#[1, 2, 'abc', "def", 4] // literal array: non posso modificarne
                         // il contenuto  in modo dinamico

Tipi di parentesi

Nelle costruzioni sintattiche del linguaggio di SuperCollider gli oggetti sono solitamente ma non necessariamente inclusi tra parentesi tonde, graffe o quadre. Ogni tipo di parentesi ha una propria funzione e nella maggioranza dei casi, definisce il tipo di data che contiene.

( )   // Parentesi tonde  -> Blocchi di codice, espressioni o argomenti
{ }   // Parentesi graffe -> Funzioni
[ , ] // Parentesi quadre -> Collezioni, Array o Liste

Nel corso dei prossimi capitoli le esploreremo una per una, parallelamente alla costruzione di brevi sequenze musicali. Prima le parentesi tonde che delimitano blocchi di codice o argomenti, poi le graffe, che delimitano le funzioni e infine le quadre che delimitano array o liste.

Parentesi tonde

In SuperCollider possiamo impiegare le parentesi tonde in tre differenti situazioni. Le prime due sono di immediata comprensione mentre la terza potrà essere assimilata appieno solo quando avremo qualche nozione in più riguardo il linguaggio informatico.

  1. Delimitare un blocco di codice per poterlo selezionare ed eseguire più facilmente:

    (  // doppio click sulla parentesi seleziona l'intero blocco
    
    rand(10.0).postln;
    exprand(0.001,23).postln;
    rrand(10,20).postln
    
    )  // doppio click sulla parentesi seleziona l'intero blocco)
    
    Quando eseguiamo un blocco di codice, l’ordine di esecuzione è quasi sempre dall’alto al basso, da sinistra a destra, riga dopo riga.

  2. Definire l’ordine di esecuzione nelle espressioni matematiche. In SuperCollider la precedenza nelle espressioni va da sinistra a destra, opposta a quella dell’ordine aritmetico. E’ sempre consigliabile forzare l’ordine di esecuzione delle espressioni, anche quando sembra superfluo.

    5 + 10 * 4;  // = 60
    5 +(10 * 4); // = 45
    (5+ 10)*4;   // = 60
    (11/4)*2;    // = 5.5
    11/(4 *2);   // = 1.375
    (11/4 *2);   // = 5.5
    

  3. Definire gli argomenti (inputs) di una funzione, una UGen o una Classe.

    rand(10);             // Abstract function
    SinOsc.ar(440,0,0.2); // UGen
    Rand(20,30);          // Classe
    

Nel riquadro seguente troviamo un esempio riassuntivo delle tre situazioni appena illustrate.

(                               // apre un blocco di codice
play{SinOsc.ar(440, 0,          // apre gli argomenti di una UGen
                (1.0*0.2)+0.01  // ordina esecuzione espressione     
               )                // chiude gli argomenti della UGen
    }
)                               // chiude il blocco di codice

All’interno di parentesi tonde o graffe possiamo definire delle variabili locali. Vediamo cosa sono e a cosa servono.

Variabili locali

Cos’è una variabile? Possiamo pensare una variabile come a una porzione di memoria del computer destinata a contenere dei dati (numeri, caratteri, audio files, synth, tabelle, array, etc.), suscettibili di modifica nel corso dell’esecuzione di un programma. Per poter distinguere la singola porzione di memoria tra le tante, dobbiamo contrassegnarla con un nome (o etichetta o indirizzo). Prima di farlo dobbiamo però dire a SuperCollider che stiamo per compiere quell’operazione, scrivendo la keyword (parola chiave o parola riservata) var (che appare blu e in grassetto) subito dopo la parentesi di apertura, sia essa tonda o graffa.

(
var...;       // blocco di codice
)

{var...;    } // funzione

Le variabili locali infatti devono essere dichiarate sempre all’inizio di un blocco di codice (parentesi tonde) oppure all’inizio di una funzione (parentesi graffe). Come nomi di variabili in SuperCollider posssiamo usare:

Come si evince dai codici precedenti, le variabili possono essere dichiarate una per ogni riga (separate da ;) e in questo caso dobbiamo riscrivere ogni volta la keyword var oppure tutte su di una sola riga separate da una virgola (chiudendo la riga sempre con ;). Utilizzare una scrittura o l’altra non cambia nulla, è solo una questione di preferenze personali (quelle che gli informatici chamano ”stile di programmazione”). Ora che le singole porzioni di memoria sono contrassegnate da un nome (etichetta o indirizzo), possiamo allocarle con uno o più dati (assegnazione della variabile).

(
var a, bpm, ciao, miao, lao, metro; // dichiarazione

a     = 92;             // assegnazione di un valore
ciao  = $b;             // assegnazione di un carattere
miao  = "ciao";         // assegnazione di una stringa
lao   = 92/60;          // assegnazione di un risultato
metro = TempoClock.new; // assegnazione di un'istanza
durs  = [1,0.5,3.0];    // assegnazione di un array
)

Dichiarazione e assegnazione dei valori di default (iniziali) possono anche essere contestuali (su una o più righe). Anche in questo caso la scelta di una o l’altra sintassi è personale e pressochè identica.

(
var a     = 92,             // dichiarazione e assegnazione virgola
    ciao  = $b,             // dichiarazione e assegnazione virgola
    miao  = "ciao",         // dichiarazione e assegnazione virgola
    lao   = 92/60,          // dichiarazione e assegnazione virgola
    metro = TempoClock.new, // dichiarazione e assegnazione virgola 
    durs  = [1,0.5,3.0];    // punto e virgola (da 'var' fino a qui
)                           // per SC e' una sola riga)

Dopo aver dichiarato e assegnato i dati a una variabile, possiamo richiamarli nel codice successivo scrivendo solo la lettera o la parola che li contrassegna (nome, etichetta o indirizzo).

(
var a,b,c;     // dichiarzione

    a = 92;    // assegnazioni
    b = "ciao";  
    c = 92/60; 

a * 2;         // richiama il valore ed esegue l'operazione riportando 184 nella Post window
b.postln;      // scrive ''ciao'' nella Post window
(c+3).postln;  // 92/60+3 = 4.533
)

Il titolo di questo Paragrafo è ’Variabili locali’ ma cosa significa ’locali’? In SuperCollider ci sono due tipi di variabili: locali e globali (chiamate anche variabili ambientali). Le prime (quelle incontrate finora) sono definite e assegnate all’interno di blocchi di codice (parentesi tonde) o funzioni (parentesi graffe) e sono precedute dalla keyword var. Sono valide solo all’interno delle parentesi entro le quali sono poste e questa loro caratteristica si chiama scope della variabile.

(
var f,metro;  // dichiara
f = 23;       // assegna
metro = 10;   // assegna

f.postln;     // richiama
metro.postln; // richiama
)

f.postln; // se eseguiamo questa riga, non riconosce la variabile 'f' perche non e inclusa tra
          // le parentesi dove e stata dichiarata e assegnata

(
var pinco = 12;
var pallino = 34.5;
var a = "ciao";
a.postln;
pallino.postln;
pinco.postln;
)

Grazie al fatto di essere locali e di valere solo all’interno dello scope, le stesse lettere o parole possono essere assegnate a valori o dati differenti se all’interno di blocchi di codice o funzioni differenti, anche se racchiusi globalmente in un unico blocco:

(
{var a=100, bum=120; (bum + a).postln}.value;
{var a=123, bum=345; (bum + a).postln}.value
)

In questo caso ad esempio la variabile a nella riga 2 è assegnata a 100, mentre nella riga 3 a 345. Anche se hanno lo stesso nome sono due variabili differenti e indipendenti, in quanto incluse all’interno di due funzioni diverse. Un consiglio riguardo la scrittura delle variabili locali è quello di separare al dichiarazione con l’assegnazione in quanto in alcuni case compere le due operazioni contemporaneamente potrebbe creare problemi. Prima dichiararle e poi assegnarle.

(
var a, bpm, ciao, miao, lao, metro; // dichiarazione

a     = 92;             // assegnazione di un valore
ciao  = $b;             // assegnazione di un carattere
miao  = "ciao";         // assegnazione di una stringa
lao   = 92/60;          // assegnazione di un risultato
metro = TempoClock.new; // assegnazione di un'istanza 
durs  = [1,0.5,3.0];    // assegnazione di un array
)

Variabili globali e ambientali

Le Variabili globali invece sono generalmente scritte all’inizio del file, al di fuori di eventuali parentesi tonde o graffe. Non bisogna usare la keyword var e se utilizziamo parole come etichette, dobbiamo anteporre il segno ~ (tilde). Possono essere richiamate ovunque in tutto il codice successivo, sia all’interno che all’esterno di parentesi tonde o graffe.

a = 23;         // lettere
~metro = 10;    // parole
                // possiamo richiamarle:

~metro.postln;  // sia fuori dai blocchi di codice

(               
a + ~metro;     // che all'interno di diversi... 
)                
(
~metro * a;     // ...blocchi di codice
)

Ricordiamoci che per dichiarare ed assegnare una variabile dobbiamo eseguire la riga di codice corrispondente. Se nelle variabili locali questa operazione è sottintesa (i blocchi di codice servono proprio a eseguirne il contenuto più facilmente) nelle variabili globali dobbiamo compiere l’operazione separatamente e prima di richiamarle nel codice successivo. Se all’interno di un patch usiamo lo stesso nome sia per contrassegnare una variabile globale che una locale, quest’ultima ha la precedenza all’interno dello scope.

a = 100;      // globale

(
var a = 12.5; // all'interno dello 'scope' la variabile 
              // locale ha precedenza 
    a.postln  // 12.5
)

a.postln      // 100

Le righe di codice dove vengono assegnate le variabili (sia locali che globali) sono una prima eccezione all’ordine di esecuzione ’da sinistra a destra’. In questo caso SuperCollider prima esegue tutto ciò che è scritto dopo il simbolo '=' e successivamente lo assegna al nome che sta prima dello stesso simbolo.

Queste variabili sono anche chiamate ambientali (currentEnvironment) perchè tutto il patch è considerato un ambiente di programmazione ed esse sono valide all’interno di tutto quest’ambiente. E’ consigliabile utilizzare le variabili globali (soprattutto le lettere) solo in files semplici. Il valore di una variabile, infatti, è solitamente modificato più volte nel corso dell’esecuzione del codice (riassegnato). Per questo motivo nel caso di algoritmi complessi o in files molto lunghi è facile perdere il controllo su questi aggiornamenti di valore. Se scriviamo ed eseguiamo currentEnvironment in SuperCollider otteniamo informazioni (nella Post window) riguardo le variabili globali già assegnate.

currentEnvironment;  // se eseguiamo riporta informazioni sulle variabili gia assegnate 

Riassegnazioni

A cosa servono le variabili? principalmente ma non solo, servono a strutturare e rendere il codice più leggibile. Facciamo un esempio. Se volessimo ottenere i risultati di tutte le quattro operazioni matematiche principali su due numeri potremmo scrivere.

(
("somma:      "++ (10 + 12)).postln;
("sottrai:    "++ (10 - 12)).postln;
("moltiplica: "++ (10 * 12)).postln;
("dividi:     "++ (10 / 12)).postln
)

Se eseguiamo l’intero blocco possiamo leggere i risultati nella Post window. E’ un codice corretto, ma nel caso in cui volessimo cambiare i due valori per ottenere altri risultati dovremmo riscriverli quattro volte (due per ogni operazione) e non è decisamente pratico. Se invece decidiamo di assegnarli a due variabili locali:

(
var a = 10, b = 12; // dichiarazione e assegnazione

("somma:      "++ (a + b)).postln;
("sottrai:    "++ (a - b)).postln;
("moltiplica: "++ (a * b)).postln;
("dividi:     "++ (a / b)).postln
)

Per gli stessi motivi le variabili (sia locali che globali) sono molto utili nel caso volessimo separare i valori musicali di alto livello (che potremmo voler modificare più volte per sperimentare cambiamenti musicali) da parti di codice di basso livello che servono solo ad effettuare le operazioni degli algoritmi che utilizziamo per ottenere un risultato musicale. Torneremo spesso su questo argomento.

Ricordando che SuperCollider esegue un blocco di codice dall’alto al basso, riga dopo riga, in un patch le variabili, siano esse locali o globali possono essere riassegnate (sovrascritte) più volte:

(
a = 10 + 3; // assegna
a.postln;   // stampa '13'
a = 999;    // riassegna, sostituendo il valore precedente
a.postln;   // stampa '999'
)

Sfrutteremo questa caratteristica in diverse situazioni. La più esemplificativa e frequente consiste nell’usare la variabile stessa (solitamente locale) per auto-aggiornarsi creando un contatore:

a = 0;       // prima assegnazione 
a = a + 1;   // contatore
a = a + 2.5; // passo di 2.5

Ogni volta che eseguiamo la riga 2 il valore assegnato alla variabile a incrementa di 1 mentre se eseguiamo la riga 3 l’incremento sarà di 2.5. Osserviamo questo processo nel dettaglio:

Parentesi graffe (funzioni)

In SuperCollider tutto ciò che è racchiuso tra parentesi graffe è una funzione. Ma cos’è una funzione? Una delle possibili definizioni dice che è un particolare costrutto sintattico che permette di raggruppare al suo interno una sequenza di istruzioni o operazioni che a partire da determinati input restituiscono determinati output.

image not found

Da questo il nome. I valori in uscita (output) sono ”in funzione” di quelli in entrata (input). Per capire meglio pensiamo alla funzione matematica che descrive una cosinusoide:

y = cos(x)

Ogni volta che modifichiamo il valore x (input) e valutiamo (eseguiamo) la funzione otteniamo un valore y (output) differente.

(
cos(0).postln; // y = 1.00000000000000
cos(1).postln; // y = 0.54030230586814
cos(2).postln; // y = -0.41614683654714
cos(3).postln; // y = -0.98999249660045
cos(4).postln; // y = -0.65364362086361
cos(5).postln; // y = 0.28366218546323
)

Se consideriamo i valori in input come ascisse di un piano cartesiano e i valori in output come ordinate, otterremo un grafico che rappresenta una cosinusoide:

image not found

Scriviamo ora nell’interprete di SuperCollider la seguente funzione ed eseguiamo il codice.

{2 + 3}

Nella post window non compare il risultato dell’addizione ma una scritta che riporta 'a Function'. Questo perchè una funzione per restituire un risultato in output deve essere valutata da un metodo. Ma cos’è un metodo? I metodi sono istruzioni che diamo agli oggetti di SuperCollider. Seguono il nome e sono separati da quest’ultimi da un punto: oggetto.metodo. Nei codici illustrati finora per esempio abbiamo incontrato spesso il metodo '.postln' la cui funzione è comunicare all’oggetto che lo precede la seguente istruzione: stampa nella post window cosa sei o il tuo valore.

(
((3*2)/4).postln;
"ciao".postln;
TempoClock(3).postln;
2.postln;
[2,34.5,56].postln;
SinOsc.ar.postln;
)

Torniamo alle funzioni. Uno dei metodi con i quali possiamo valutarle è '.value' che come termine fornisce un duplice ausilio mnemonico: possiamo ricordarlo sia come ’valore’ ovvero ’restituisci il risultato in output di questa funzione’ che come ’vàluta’ ovvero ’vàluta questa funzione e restituisci il risultato in output’.

{2 + 3}.value

La funzione precedente in realtà non ha molto senso in quanto i due elementi che la compongono (inputs) sono dei valori costanti. Avremmo potuto semplicemente scrivere l’operazione e assegnarla a una variabile.

a = 2 + 3;
a.postln;
a.postln; // etc.
a = {2 + 3};
a.value;
a.value; // etc.

Come possiamo osservare variabili e funzioni hanno caratteristiche simili. Questo perchè uno degli scopi principali di entrambe in informatica è quello di rendere più ordinato e comprensibile il codice, ovvero realizzare quella che si chiama formalizzazione di un pensiero. Ci sono però anche alcune differenze tra le quali la più importante e discriminante consiste nel fatto che:

Per modificare i valori interni di una funzione dall’esterno, in SuperCollider dobbiamo effettuare quattro operazioni:

  1. assegnare la funzione in questione a una variabile.
    f = {"funzione"}; 
  2. dichiarare al suo interno degli argomenti.
    f = {arg a, b;}; 
  3. definire le operazioni da effettuare sugli argomenti.
    (
    f = {arg a, b;
         a - b};  
    )
    
  4. eseguire tre operazioni contestuali:
    • richiamare la funzione specifica attraverso l’etichetta assegnata alla variabile,
    • modificare dinamicamente i valori degli argomenti (inputs)
    • valutarla ottenendo il risultato (output).
    (
    f = {              // assegna a una varibile
         arg a, b;     // dichiara gli argomenti
         a - b};       // definisce le operazioni da effettuare
    )
    
    f.value(5, 3);     // richiama, modifica valori e valuta
    f.value(2.5, 5.1); // etc.
    

L’elemento nuovo in questo esempio è costituito dagli argomenti. Osserviamoli nel dettaglio.

Argomenti delle funzioni

Possiamo pensare gli argomenti di una funzione come variabili locali alle quali possiamo modificare dinamicamente il valore non solo all’interno del blocco di codice che le contiene (in questo caso lo scope di una funzione) ma anche dall’esterno. Dobbiamo specificarli (dichiararli) all’interno della funzione stessa prima di qualsiasi altra cosa, variabili comprese e possiamo scriverli in due modi differenti ma equivalenti:

(
f = {arg a, b;         // preceduti dalla keyword arg.
         a / b};
g = {|luigi berenice|  // racchiusi tra due simboli |.
         luigi * berenice}
)

Nel primo caso dobbiamo separarli con virgole e l’ultimo deve essere seguito da un punto e virgola, nel secondo caso no. Possiamo utilizzare sia lettere che parole a nostro piacere, escludiendo quelle riservate alle keywords.

Dopo aver definito gli argomenti e stabilito quali operazioni devono essere effettuate su di essi possiamo cambiarne i valori dinamicamente utilizzando la seguente sintassi: 'funzione.value(arg1,arg2,arg3,etc.)' dove al posto di arg1,arg2, etc. scriviamo i nuovi valori dei singoli argomenti. Questi ultimi non rimangono in memoria e valgono solo per la valutazione della riga dove sono scritti.

(
f = {arg a, b; 
         a / b};

g = {|luigi berenice| 
         luigi * berenice};

f.value(100, 2).postln;     // 50
f.value(12, 20).postln;     // 0.6
g.value(1.3, 1.2).postln;   // 1.56
g.value.postln;             // errore
)

All’esecuzione di questo codice SuperCollider prima sostituisce i valori degli argomenti e dopo valuta la funzione restituendone il risultato. Gli argomenti possono essere specificati in due modi diversi:

(
f = {arg acci, bicci; acci / bicci}; // funzione

f.value(10, 2).postln;               // stile regolare
f.value(bicci: 2, acci: 10).postln;  // stile con keywords
)

Così come per le variabili, possiamo anche assegnare valori di default agli argomenti che non saranno sovrascritti da eventuali nuovi valori, ovvero saranno sostituiti solo nella singola valutazione.

(
f = {arg a = "luigi ", b = "e berenice"; 
         a++b};

g = {|luigi = 12 berenice = 34.5| 
         luigi * berenice};

f.value("gina", " e brisotto").postln; // gina e brisotto
f.value.postln;                        // luigi e berenice         
g.value(3,5).postln;                   // 15
g.value.postln                         // 415
)

All’interno di una funzione dopo gli argomenti possiamo specificare anche variabili (locali) che si comportano nel modo che abbiamo già visto. Siccome in alcune situazioni è necessario prima dichiarare i nomi di argomenti e variabili locali e dopo assegnargli valori di default, consiglio di utilizzare sempre questo schema sintattico.

(
f = {arg a, b;     // dichiaro argomenti (input)
         a = 100;  // valori di default
         b = 12;

     var piu, meno, diviso, per; // dichiaro variabili locali
         piu    = a + b,         // assegno le operazioni
         meno   = a - b,
         diviso = a / b,
         per    = a * b;

	["somma: "++piu,         // stampa output
	 "sottrazione: "++ meno,
	 "divisione: "++ diviso,
	 "moltiplicazione: "++ per].postln
    }                      
)

f.value(2, 3); // modifico dinamicamente

Riassumendo. Se vogliamo modificare valori dall’esterno di una funzione richiamata più volte nel corso della computazione (input) utilizzeremo argomenti. Se invece le modifiche restano confinate al suo interno (scope), utilizzeremo variabili locali. Possiamo passare argomenti a una funzione, valutarla e ottenere il risultato (output) invocando il metodo '.value' nella seguente forma sintattica: funzione.value(arg1,arg2,etc).

Funzioni astratte

In SuperCollider abbiamo a disposizione due modi per scrivere alcune funzioni di uso comune:

5.reciprocal   // Reciproco
r = {|a| 1/a}; 
r.value(5);

5.squared;     // Quadrato
f = {|a| a*a}; 
f.value(5);

0.5.ampdb;     // Conversioni, etc.
f = {arg a; 20*(log10(a))};
f.value(0.5);

Una funzione astratta è un oggetto che risponde a una serie di messaggi che rappresentano alcune funzioni matematiche usate frequentemente. Nel codice precedente ad esempio:

Possiamo trovare l’elenco di tutte le funzioni astratte che abbiamo a disposizione in SuperCollider nell’Help file di AbstractFunction. Se leggiamo attentamente questo Help possiamo notare che le AbstracFunctions sono suddivise in tre tipologie che variano a seconda del numero di argomenti (o messaggi) che accettano:

Differenze

Osserviamo una importante differenza tra funzioni e funzioni astratte. Eseguiamo il seguente codice:

rand(100);         // riporta ad ogni esecuzione un valore pseudo-casuale tra 0 e 100.
{rand(100)};       // riporta "a Function" e deve essere valutata
{rand(100)}.value;

Notiamo che le funzioni astratte non hanno bisogno del metodo '.value' per restituire il risultato, mentre le funzioni incluse tra parentesi graffe si. Le conseguenze di questa differenza sono illustrate nel codice seguente:

dup( rand(100), 5);  // a ogni esecuzione sceglie un numero pseudo-casuale e lo ripete 5 volte uguale...
dup({rand(100)}, 5); // a ogni esecuzione ripete 5 volte la funzione di scegliere un numero pseudo-casuale...

Se replichiamo n volte una funzione astratta ne replichiamo il risultato, mentre se replichiamo n volte una funzione inclusa tra parentesi graffe ne replichiamo il processo interno.

Notazioni

Se osserviamo attentamente il codice di esempio sui messaggi binari possiamo notare che in alcuni casi il messsaggio che diamo all’oggetto non segue la sintassi che conosciamo 'oggetto.metodo(arg)'. Manca il punto e un oggetto al quale inviare il messaggio, o meglio questo oggetto diventa parte integrante degli argomenti, per la precisione il primo. Questo perchè in SuperCollider per inviare messaggi a un oggetto abbiamo a disposizione due possibili notazioni che si equivalgono: receiver notation e functional notation.

23.rand;   // Receiver notation
rand(23);  // Functional notation

Receiver notation

E’ la notazione che segue lo schema sintattico incontrato finora. Il messaggio segue l’oggetto, separato da un punto e può avere uno o più argomenti racchiusi tra parentesi tonde.

124.postln;                               
100.rand;    
(Env.new([0,1,0.5,0],[0.1,0.2,1])).plot;
({SinOsc.ar(543,0,0.25)}).play;                
("notazione ricevente").speak; 

Functional notation

L’oggetto diventa il primo argomento del messaggio e il punto sparisce.

postln(124);   // stampa nella post window 
rand(100);     // sceglie un valore random tra 0 e 100
plot(Env.new([0,1,0.5,0],[0.1,0.2,1]));
play({SinOsc.ar(543,0,0.25)});
speak("notazione funzionale"); // Solo Mac

Praticamente tra le due notazioni non c’è alcuna differenza. Se prendiamo come esempio l’azione di stampare un oggetto nella post window (postln) possiamo pensare la prima come: oggetto.stampami e la seconda come: stampa(questo oggetto). In determinati casi il codice è più comprensibile se ne utilizziamo una, in altri l’altra. Teoricamente la receiver notation richiama un metodo programmato in precedenza all’interno di una Classe mentre la functional notation invia un messaggio a un oggetto dall’esterno come nel caso delle funzioni (AbstractFunction). Notiamo che la richiesta di compiere un’azione fatta a un oggetto può chiamarsi sia messaggio (quando utilizziamo la functional notation) che metodo (quando utilizziamo la reciver notation). Ai fini pratici sono sinonimi.