Introduzione a Mathematica

Gianluca Gorni
Università di Udine, Dipartimento di Matematica e Informatica

6. Funzioni: definizione e grafici

Ultima modifica: 3 giugno 2007

Questo file è concepito principalmente per essere letto interattivamente su schermo. Per questo, e anche per risparmiare spazio, gli output di solito sono omessi. L'utente che legge da schermo li deve rigenerare mandando in esecuzione le celle di input una alla volta.
Qualcuno vorrà stampare per leggere su carta: in tal caso apra i gruppi di celle ed esegua tutte le celle prima di stampare. Le celle si possono eseguire tutte in un sol colpo col menu Kernel>Evaluation>Evaluate Notebook

Inizializzazione

Inizializzazione consigliata:

SetOptions[$FrontEnd, 
        CommonDefaultFormatTypes -> {"Output" -> 
              TraditionalForm}];
$FormatType = TraditionalForm;

Funzioni definite con " = " (definizione immediata)

Con " = " si può assegnare un valore non solo a un simbolo, ma anche a una funzione:

f[x_]=x^2

Una volta caricata questa definizione possiamo calcolare il valore di f di qualsiasi argomento:

f[0]

f[5]

f[Sqrt[3]]

f[x + y]

Possiamo anche disegnarne il grafico, per esempio nell'intervallo fra -1 e 1:

Plot[f[x],{x, -1, 1}];

Attenzione a non mettere le sottolineature dentro Plot:

Plot[f[x_], {x, -1, 1}];

Per disegnare il grafico non era necessario avere definito la funzione. Bastava mettere la formula direttamente dentro Plot:

Plot[x^3 - x,{x, -2, 2}];

Più informazioni sui grafici di funzioni nel file numero 11.

Tornando a come definire una funzione, osservate la sottolineatura che segue il simbolo x nell'argomento di f. La sottolineatura compare solo a sinistra dell'uguale, non a destra. La sottolineatura fa interpretare l'espressione f[x_] come la "pattern" f calcolata su un qualsiasi cosa; quando il nòcciolo in séguito incontra una f avvolta attorno a una qualche espressione, assegnerà (temporaneamente) l'espressione alla variabile x, poi sostituirà questo valore nel secondo membro della definizione x^2.

Se si tralascia la sottolineatura il secondo membro viene usato soltanto quando il nòcciolo incontra letteralmente " f[x] ", mentre lascierà intatte f[1], f[y] ecc.:

Clear[f];
f[x]=x^2

f[x]

f[1]

f[y]

Funzioni definite con " := " (definizione ritardata)

È molto più comune definire le funzioni con " := " ("definizione ritardata") invece che con il semplice " = ":

g[x_]:=2  x^3 - x

g[0]

g[1]

1 + g[x + y]

La differenza fra i due modi di definire è abbastanza sottile. Quando si definisce con " = " (definizione "immediata"), il secondo membro della definizione viene valutato all'istante, e alla pattern f[x_] viene associato il risultato della valutazione. Invece quando si usa " := "  (definizione "ritardata") il secondo membro viene memorizzato congelato alla lettera; quando il nòcciolo troverà g[qualcosa] prima sostituirà il qualcosa nel secondo membro ancora congelato, e solo dopo scongelerà e farà la valutazione.
Nella maggior parte dei casi il risultato dei due modi di definire è lo stesso. Tuttavia la definizione ritardata è raccomandata nella grande maggioranza dei casi. Il motivo è questo: supponiamo di aver fatto dei calcoli, durante i quali avevamo assegnato a x un valore, per esempio

x = 3;

Più avanti ci siamo dimenticati di aver dato quest'assegnazione. Vogliamo definire una funzione usando la variabile x; e malauguratamente usiamo la definizione immediata:

h[x_]=x^2

Accidenti! Il secondo membro della definizione è stato valutato all'istante, col valore che la x per puro caso aveva in questo momento. Così ora la h vale sempre 9:

h[0]

h[2]

Per vedere che regole sono memorizzate con h basta valutare ?h:

?h

Se invece definiamo la h con la definizione ritardata

Clear[h];
h[x_]:=x^2

La formula al secondo membro x^2 non viene valutata subito, ma congelata in memoria. Da quel momento in poi ogni singola volta che h viene usato, nella formula congelata alla x viene sostituito il valore che si incontra dentro h[qualcosa], dopodiché si scongela e si calcola:

h[1]

h[2]

h[y]

?h

Proprio il pericolo di assegnazioni dimenticate di variabili fa raccomandare di usare la definizione ritardata (salvo controindicazioni in casi speciali).

Altro esempio: assegnamo 1+1 come valore di due simboli, uno con definizione immediata, l'altro con definizione ritardata:

a = 1 + 1;
b := 1 + 1;

Con la definizione immediata Mathematica associa al simbolo il risultato dell'operazione 1+1:

?a

Con la definizione ritardata Mathematica associa al simbolo testualmente ciò che abbiamo scritto, senza eseguire calcoli:

?b

I calcoli vengono effettuati al momento in cui il simbolo viene usato in un calcolo:

b

Se in una formula la b compare più volte, lo scongelamento avviene altrettante volte, anche milioni di volte se càpita, e può sprecare diverso tempo. In tali casi potrebbe convenire una definizione immediata.

Numeri casuali e definizioni

Un caso in cui la distinzione fra definizioni ritardate e immediate risalta bene è con i numeri casuali. Il comando Random[] produce un numero in virgola mobile casuale fra 0 e 1:

Random[]

Se assegnamo un numero casuale al simbolo c con definizione immediata

c=Random[]

d'ora in poi, fino ad avviso contrario, il valore di c è fissato:

c

c

c == c

Se invece diamo una definizione ritardata

d:=Random[]

ogniqualvolta il nòcciolo incontra d lo sostituisce con un valore casuale ricalcolato di fresco:

d

d

{d, d}

d == d

d - d

Ora, ci sono delle situazioni in cui è più appropriato il primo comportamento e altre in cui è meglio il secondo. Se per esempio vogliamo fare uno studio del singolo numero casuale sarà meglio la definizione immediata:

c > 1/2

Sqrt[c] >  .1

Se invece vogliamo per esempio fare una lista di numeri casuali useremo la definizione ritardata:

{d, d, d, d, d}

Una variabile che ogni volta che viene calcolata genera un intero casuale fra -4 e 4:

r := Random[Integer, {-4, 4}];

Un polinomio casuale con radici intere:

Expand[(x - r) (x - r)]

Altro esempio di differenza fra immediato e ritardato

Supponiamo di voler definire f[x] come la derivata di (x^2) rispetto a x, ma facendo fare il calcolo a Mathematica. Partiamo con una definizione immediata:

Clear[f, x];
f[x_] = D[(x^2), x];

Questa f funziona normalmente:

f[y]

f[1]

f[-5]

Proviamo con una definizione ritardata:

Clear[g, x];
g[x_] := D[(x^2), x];

Proviamo:

g[y]

g[1]

g[-5]

Cosa succede????
La spiegazione è questa: quando si chiede g[1], Mathematica prende il secondo membro della definizione D[(x^2), x], e sostituisce x con 1, ottenendo D[(1^2), 1]. Ora, la derivata di (1^2) rispetto a 1 non ha senso.

Nella definizione ritardata il secondo membro viene conservato intatto. Quando se ne chiede quanto vale per un dato valore di x, questo valore di x  viene prima sostituito nel secondo membro, e poi si esegue il calcolo.

Nella definizione immediata il secondo membro viene precalcolato subito fin dove ha senso. Quando se ne chiede quanto vale per un dato valore di x, questo valore di x  viene prima sostituito nel secondo membro già precalcolato, e poi si esegue il calcolo col nuovo valore.

E' una questione di ordine delle operazioni.

?f

?g

Morale: occasionalmente può succedere che la definizione immediata sia obbligatoria per avere l'effetto desiderato. Ma difficilmente la cosa càpita a un principiante.

Successioni ricorsive

Consideriamo la successione definita per ricorsione   {({{a[1] == 1}, {a[n] == 2  a[n - 1]}}). Per calcolarla con Mathematica basta scrivere

a[1] = 1;
a[n_] := 2  a[n - 1];

(Notare la definizione ritardata nella seconda riga; la prima riga potrebbe essere anche ritardata, è indifferente). Con questo possiamo calcolare a[n] quando n è intero ≥1:

a[2]

a[3]

a[4]

a[12]

Attenzione: non si può definire a[n_+1]:=2  a[n]. Infatti Mathematica applicherebbe questa regola soltanto quando dovesse calcolare il valore di a su un'espressione che sia esplicitamente della forma qualcosa + 1. Mathematica non riscrive automaticamente qualcosa come (qualcosa - 1) + 1.

Altro esempio: il fattoriale (anche se è già predefinito):

fatt[0] = 1;
fatt[n_]:=n*fatt[n - 1];

fatt[12]

La successione di Fibonacci:

fib[0] = 1;
fib[1] = 
        1;
fib[n_]:=fib[n - 1] + fib[n - 2];

fib[5]

Si può anche usare la più familiare notazione con gli indici in basso:

Clear[b];
(Subscript[b, 0]) = 1;
(Subscript[b, n])_ := 2  (Subscript[b, n - 1])

(Subscript[b, 3])

Per cancellare una successione definita con gli indici in basso non basta semplicemente Clear: ci vuole il più drastico Remove:

Clear[b];
(Subscript[b, 0])

Remove[b];
(Subscript[b, 0])

Come far ricordare i valori calcolati

In una successione ricorsiva come   {({{a[1] == 1}, {a[n] == 2  a[n - 1]}})per calcolare a[4] bisogna avere il valore di a[3]; per calcolare a[3] ci vuole a[2] eccetera. Supponiamo di avere già calcolato a[32]. Mathematica non si è annotata che a[32] ha quel valore. Quindi quando le chiedo di trovare a[33] si ricalcola daccapo a[1], a[2],  ...  , a[32], perdendo del tempo.
C'è un semplice trucco per far ricordare a Mathematica i valori già calcolati, per sveltire i conti successivi, ed è di fare una doppia assegnazione, una immediata all'interno di una ritardata:

Clear[a];
a[1] = 
        1;
a[n_]:=a[n] = 2  a[n - 1]

In questo modo quando viene chiesto il valore di a[3], oltre a venire calcolato il valore, questo viene assegnato all'espressione a[3], e usato nei conti successivi. Per esempio

a[3]

?a

Notate che la sottolineatura nella definizione a[n_]:=a[n]=2a[n-1] c'è soltanto al primo membro, non dopo.

Funzioni di più variabili

Si definiscono funzioni di più variabili dando a ognuna la sua sottolineatura:

f[x_,y_]:=x^2 + y^2

f[0, 1]

f[x + y, x - y]

Il grafico di una funzione di due variabili si fa con Plot3D:

Plot3D[f[x, y],{x, -1, 1},{y, -1, 1}];

Anche qui non era necessario aver definito f[x,y]; si poteva inserire direttamente la formula:

Plot3D[Sin[x*y],{x, -Pi, Pi},{y, -1, 1}];

Esercizi

Disegnare i grafici delle funzioni (Sqrt[x]), ((x - 1)/((x^2) + 2)), Sin[x]/x per x fra 0 e 2.

Disegnare il grafico di ((x y)/((x^2) + (y^2) + 1)) per x e y fra -1e 1.

Disegnare i grafici delle due funzioni (con lo stesso secondo membro ma una ritardata e l'altra immediata): f[x_]:=(x^2) + Random[] e di g[x_]=(x^2) + Random[] per x fra -1 e 1. Come spiegate la differenza?

Definire f[x_, y_]:=Factor[(x^2) - (y^2)] e g[x_, y_]=Factor[(x^2) + (y^2)] (stesso secondo membro ma una ritardata e l'altra immediata), e poi calcolare f[(x^2) - 1, (y^2) - 1]. Il risultato è identico? È equivalente? Come lo spiegate?