RISTAMPATO DA SIGPLAN NOTICES, Dicembre 1986

Una Visione d'Insieme di Miranda


David Turner
Computing Laboratory
University of Kent
Canterbury CT2 7NF
ENGLAND

Miranda è un sistema avanzato per la programmazione funzionale che gira sotto il sistema operativa UNIX (*). L'obiettivo del sistema Miranda è di fornire un moderno linguaggio funzionale incastonato in un ambiente di sviluppo di `qualità industriale'. Attualmente lo si sta utilizzando ad un numero crescente di siti per insegnare la programmazione funzionale e come veicolo per creare velocemente dei prototipi di software. Lo scopo di questo breve articolo è quello di dare una veloce occhiata alle caratteristiche principali di Miranda. Gli argomenti che tratteremo, in ordine, sono:

(*) Nota: UNIX è un marchio di AT&T Bell Laboratories, Miranda è un marchio di Research Software Ltd.

I concetti di base

Il linguaggio di programmazione Miranda è puramente funzionale - non ci sono né effetti collaterali né caratteristiche imperative di alcun genere. Un programma (che non chiamiamo un programma, ma "uno script") è composto di una collezione di equazioni che definiscono le varie funzioni e le strutture di dati che vogliamo elaborare. L'ordine in cui le equazioni vengono date non è, in generale, significativo. Ad esempio, la definizione di un'entità non è costretta a precedere il suo primo utilizzo. Ecco un'esempio molto semplice di uno script in Miranda:

        z = quad x / quad y
        quad n = n * n
        x = a + b
        y = a - b
        a = 10
        b = 5
Nota l'assenza di bagaglio sintattico - Miranda è, per disegno, piuttosto terso. La dichiarazione dei tipi non è obbligatorio, anche se (vedi sotto) il linguaggio è fortemente tipato ("strongly typed"). Non ci sono dei punto e virgola alla fine delle definizioni - l'algoritmo per il "parsing" fa un uso intelligente della disposizione del testo. Nota che la notazione per l'applicazione di un a funzione è la semplice juxtaposition, come in "quad x". Nella definizione della funzione quad, "n" è un parametro formale - il suo raggio d'azione ("scope") si limita all'equazione in cui accade (mentre gli altri nomi introdotti sopra hanno come raggio d'azione l'intero script).

La struttura di dati di più comune uso è la lista che, in Miranda, si scrive con le parentesi quadre e le virgole, p.es.:

        giorni_feriali = ["Lun","Mart","Merc","Giov","Ven","Sab"]
        giorni = giorni_feriali ++ ["Dom"]

Le liste si possono aggiungere l'una all'altra con l'operatore "++". Fra le altre operazioni utili sulle liste sono il ":" infisso, che prefigge un'elemento all'inizio di una lista, "#", che prende la lunghezza di una lista, e il "!" infisso, che compie la "subscription". Così, ad esempio, 0:[1,2,3] ha il valore di [0,1,2,3], #days vale 7 e days!0 vale "Lun".

Inoltre, c'è un'operatore "--" che fa la sottrazione fra liste. Per esempio, [1,2,3,4,5] -- [2,4] vale [1,3,5].

C'è anche una notazione abbraviata che utilizza ".." per le liste cui elementi formano una serie aritmetica. Ecco, per mo' di esempio, le definizioni della funzione fattoriale e di un numero "risultato" che è la somma dei numeri dispari fra 1 e 100 (le funzioni sum e product fanno parte della libreria):

        fatt n = product [1..n]
        risultato = sum [1,3..100]

Gli elementi di una lista devono essere tutti dello stesso tipo mentre una sequenza di elemento di tipi misti si chiama un "tuple", e si scrive con le parentesi tonde piuttosto che quadre. Un esempio:

        impiegato = ("Rossi",True,False,39)

Le "tuple" sono analoghe ai record di Pascal (mentre le liste sono analoghe alle array). Non si possono "subscript" i tuples - i loro elementi vengono estratti attraverso "pattern matching" (vedi sotto).

L'Ambiente Miranda

Il sistema Miranda è interattivo e giro sotto UNIX come sottosistema autosufficiente. La sua azione basilare è di valutare le espressioni fornite dall'utente al terminale, nell'ambiente stabilita dallo script corrente. Ad esempio, la valutazione di "z" nel contesto dello script dato sopra darebbe come risultato "9".

Il compilatore di Miranda lavora in congiunzione con un editor (di solito si tratta di "vi", ma lo si può impostare a qualunque editor a scelta dell'utente) e gli script vengono ricompilati automaticamente dopo ogni edit, segnalando immediatamente gli errori di sintassi o di tipo. Il sistema di tipi polimorfici permette di rilevare un'elevata proporzione degli errori logici al momento della compilazione.

C'è una libreria piuttosto grande di funzioni standard, più un mauale di riferimento in linea che documenta tutti gli aspetti del sistema. L'interfaccia a UNIX è buona, e permette ai programmi in Miranda di prendere dati da, e mandare dati ad archivi arbitrari in UNIX, si possono evocare dei programmi in Miranda direttamente dallo shell di UNIX, ed è anche possibile abbinarli, via i "pipe" di UNIX, a dei processi scritti in altri linguaggi.

Le Equazioni con guard e la Struttura a Blocchi

Un'equazione può avere più lati destri alternative, distinte da "guardie" - la guardia si scrive a destra dopo una virgola. Ad esempio, la funzione che trova il maggiore divisore comune si può scrivere:

        mdc a b = mdc (a-b) b, if a>b
                = mdc a (b-a), if a<b
                = a, if a=b

The last guard in such a series of alternatives can be written "Otherwise", piuttosto che "If condizione", per indicare una clausola di difetto(*).

È anche permesso introdurre delle definizioni locali sul lato destro di una definizione, attraverso una clausola "where". Considera, per esempio, la seguente definizione di una funzione che risolve le equazioni quadratiche (o fallisce o restituisce una lista di uno o di due radici reali):

        quadrisol a b c = error "radici complessi", if delta<0
                        = [-b/(2*a)],               if delta=0
                        = [-b/(2*a) + radice/(2*a),
                           -b/(2*a) - radice/(2*a)], if delta>0
                          where
                          delta = b*b - 4*a*c
                          radice = sqrt delta

Le clausole where si possono annidare ("nest"), senza limite di profondità, permettendo di organizzare i programmi in Miranda con una struttura a blocchi annidati ("nested"). L'indentazione dei blocchi interni è obbligatorio, poiché le informazioni date dalla disposizione del testo vengono utilizzate dal "parser".

(*) Nota: Per mantenere la compatibilità con delle versioni precedenti di Miranda, l'utilizzo della parola "if" nelle guardie è opzionale.

"Pattern Matching"

Si può definire una funzione dando più equazioni alternative, distinte dall'utilizzo di pattern diversi nei parametri formali. Con questo si ha un altro metodo per analizzare i casi, che spesse volte risulta più elegante rispetto all'utilizzo delle guardie. Qui diamo degli esempi semplici di "pattern matching" sui numeri naturali, sulle liste, e sulle "tuple". Ecco una (altra) definizione della funzione fattoriale, ed una definizione della funzione di Ackermann:

        fatt 0 = 1
        fatt (n+1) = (n+1)*fatt n

        ack 0 n = n+1
        ack (m+1) 0 = ack m 1
        ack (m+1) (n+1) = ack m (ack (m+1) n)

Ecco una definizione (ingenua) di una funzione per la computazione dell'ennesimo numero di Fibonacci:

        fib 0 = 0
        fib 1 = 1
        fib (n+2) = fib (n+1) + fib n

Ecco degli esempi semplici di funzioni definiti con il "pattern matching" su delle liste

        sum [] = 0
        sum (a:x) = a + sum x

        product [] = 1
        product (a:x) = a * product x

        reverse [] = []
        reverse (a:x) = reverse x ++ [a]

Anche l'accesso agli elementi di una "tuple" si ha attraverso il "pattern matching". Ad esempio, le funzioni di selezione per le 2-tuple si possono definire così:

        fst (a,b) = a
        snd (a,b) = b

Come esempi finali, diamo le definizioni di due funzioni dalla libraria di Miranda, take ("prendi") e drop ("scarta"), che restituiscono i primi n elementi di una lista, e il resto della lista senza i primi n elementi, rispettivamente

        take 0 x = []
        take (n+1) [] = []
        take (n+1) (a:x) = a : take n x

        drop 0 x = x
        drop (n+1) [] = []
        drop (n+1) (a:x) = drop n x

Nota and le due funzioni sono definiti in tal modo che la seguente identità sia sempre valida - "take n x ++ drop n x = x" - compreso il caso patologico in cui la lunghezza di x è inferiore ad n.

Currying and le Funzioni di Ordine Superiore

Miranda è un linguaggio pienamente di ordine superiore - le funzioni sono dei cittadini di prima classe, e si possono sia passare come parametri sia restituire come risultati. L'applicatione delle funzione si associa a sinistra, e dunque quando scriviamo "f x y", viene interpretato come "(f x) y", il ché significa che l'applicazione di f ad x dà come risultato una funzione, che viene poi applicata ad y. Il lettore potrà mettere la sua comprensione delle funzioni di ordine superiore alla prova calcolando il valore di "risposta" nel seguente script:

        risposta = due_volte due_volte due_volte suc 0
        due_volte f x = f (f x)
        suc x = x + 1
Nota che, in Miranda, ogni funzione di due parametri o più è in realtà una funzione di ordine superiore. Questo è molto utile poiché ammette l'applicazione parziale. Ad esempio, "member" è una funzione dalla libreria tale che "member a x" determina se la lista x contiene l'elemento a (restituendo True o False, secondo il caso). Applicando member parzialmente, possiamo derivare molti predicati utili, come
        vocale = member ['a','e','i','o','u']
        cifra = member ['0','1','2','3','4','5','6','7','8','9']
        mese = member ["Gen","Feb","Mar","Apr","Giu","Lug","Ago","Set", 
                        "Ott","Nov","Dic"]

Per un'altro esempio della programmazione di ordine superiore, considera la funzione foldr, cui definizione è

        foldr op k [] = k
        foldr op k (a:x) = op a (foldr op k x)

Tutte le funzioni standard per trattare le liste si possono ottenere applicando foldr parzialmente. Esempi:

        sum = foldr (+) 0
        product = foldr (*) 1
        reverse = foldr postfix []
                  where postfix a x = x ++ [a]

La Comprensione delle Liste

La comprensione delle liste fornisce una sintassi concisa per una classe piuttosto generale di iterazioni sulle liste. La sintassi si presta dalla notazione analoga usata nella teoria degli insiemi (in cui si chiama la "comprensione di un'insieme"). Un esempio semplice di una comprensione di una lista sarebbe:

        [ n*n | n <- [1..100] ]
Questa è una lista che contiene (in ordine) i quadrati di tutti i numeri da 1 fino a 100. Quest'espressione si leggerebbe ad alta voce: "lista di tutti le n per n tale che n sia tratta dalla lista da 1 a 100". Nota che "n" è una variable locale dell'espressione. Il costrutto per legare i variabili, a destra della barra, si chiama un "generatore" - il segno "<-" denota che la variabile introdotta alla sua sinistra percorre tutti gli elementi della lista alla sua destra. La forma generale di una comprensione di una lista in Miranda è:
        [ corpo | qualificatori ]
dove ogni qualificatore o è un generatore della forma var<-espr, o è un filtro, ovvero un'espressione booleana usata per restringere la gamma dei variabili introdotti dai generatori. Quando sono presente due o più generatori, vengono separati da punto-e-virgola. Un'esempio della comprensione di una lista con due generatori si ha nella seguente definizione di una funzione che restituisce un elenco di tutte le permutazioni di una data lista.
        permut [] = [[]]
        permut x = [ a:y | a <- x; y <- perms (x--[a]) ]

L'uso di un filtro si vede nella seguente definizione di una funzione che prende un numero e restituisce una lista di tutti i suoi fattori,

        fattori n = [ i | i <- [1..n div 2]; n mod i = 0 ]

Spesso, le comprensioni delle liste permettono una concision notevole d'espressione. Diàmone due esempi. Ecco una definizione in Miranda dell'alrogitmo "quicksort" di Hoare, come metodo per ordinare una lista,

        sort [] = []
        sort (a:x) = sort [ b | b <- x; b<=a ]
                     ++ [a] ++
                     sort [ b | b <- x; b>a ]

Ora diamo una soluzione in Miranda del problema delle otto regine. Dobbiamo posizionare otto regine sulla scacchiera in modo che nessuna regina dia scacco ad un'altra. Siccome ogni soluzione deve per forza avere esattamente una regina in ogni colonna, una rappresentazione adatta alla scacchiera sarebbe una lista di interi che dànno il numero della riga sulla wuale si trova la regina in ogni colonna successiva. Nel seguente script, la funzione "regine n" restituisce tutti i modi sicuri di posizionare delle regine sulle prime n colonne. Si ottiene un elenco di tutte le soluzioni al problema delle otto regine, quindi, stampando il valore di "regine 8".

        regine 0 = [[]]
        regine (n+1) = [ q:b | b <- regine n; q <- [0..7]; sicura q b ]
        sicura q b = and [ ~da_scacco q b i | i <- [0..#b-1] ]
        da_scacco q b i = q=b!i \/ abs(q - b!i)=i+1

Lazy Evaluation and Infinite Lists

Il meccanismo di Miranda per la valutazione è pigro, nel senso che nessuna sottoespressione viene valutata fin quando non si sappia che il suo valore sarà richiesto. Una delle conseguenze di ciò è che diventa possibile definire delle funzioni non stretti (il ché significa che sono capaci di restituire una risposta anche se uno dei loro parametri non è definito). Ad esempio, possiamo definire una funzione condizionale come segue,

        cond True x y = x
        cond False x y = y
ed utilizzarlo in seguito in delle situazioni come "cond (x=0) 0 (1/x)".

L'altra conseguenza principale della valutazione pigra è che diventa possibile scrivere delle definizioni di strutture infinite di dati. Ecco degli esempi di definizioni in Miranda di alcune liste infinite (nota che c'è una forma modificata della notazione ".." per le progressioni aritmetiche senza fine)

        uni = 1 : uni
        ripete a = x
                   where x = a : x
        naturali = [0..]
        dispari = [1,3..]
        quadrati = [ n*n | n <- [0..] ]
        perfetti = [ n | n <- [1..]; sum(fattori n) = n ]
        primi = setaccia [ 2.. ]
                where
                setaccia (p:x) = p : setaccia [ n | n <- x; n mod p > 0 ]

Un'applicazione interessante delle liste infinite è di farli agire come tabelle per implementare una cache dei valori di una funzione. Per esempio, la nostra prima definizione ingenua di "fib" si può migliorare dalla complessità esponenziale a quella lineare cambiando la ricorsione per far uso di una tabella di ricerca, così:

        fib 0 = 1
        fib 1 = 1
        fib (n+2) = flista!(n+1) + flista!n
                    where
                    flista = map fib [ 0.. ]

Un altro utilizzo importante delle liste infinite è quando ci permettono di scrivere dei programmi funzionali che rappresentono delle reti di processi comunicanti. Considera, ad esempio, il problema dei numeri di Hamming - dobbiamo stampare in ordine ascendente tutti i numeri della forma 2^a*3^b*5^c, per a,b,c>=0. Una bella soluzione a questo problema si ha in termini di processi comunicanti, che si può esprimere in Miranda come segue

        hamming = 1 : combina (f 2) (combina (f 3) (f 5))
                  where
                  f a = [ n*a | n <- hamming ]
                  combina (a:x) (b:y) = a : combina x (b:y), if a<b
                                      = b : combina (a:x) y, if a>b
                                      = a : combina x y,     otherwise

Il "Typing" Polymorphico Forti

Miranda è fortemente "typed". Ciò significa che ogni espressione ed ogni sottoespressione è di un tipo che si può dedurre al momento della compilazione, e qualunque "inconsistency" nella struttura tipologico di uno script dà come risultato un messaggio d'errore al momento della compilazione. Qui diamo un breve sommario della notazione di Miranda per i suoi tipi.

Ci sono tre tipi primitivi, che si chiamano num, bool e char. Il tipo num comprende gli interi e i numeri a virgola mobile (la distinzione tra interi e numeri a virgola mobile viene gestita al momento dell'esecuzione - questo non viene considerata una differenza di tipo). Ci sono due valori di tipo bool, chiamati True e False. Il tipo char comprende l'insieme di caratteri ascii - i costanti di tipo carattere si scrivono fra virgolette singole, utilizzando le convenzioni di C, e.g. 'a', '$', '\n' ecc.

Se T è un tipo, allora [T] è il tipo delle liste cui elementi sono di tipo T. Ad esempio, [[1,2],[2,3],[4,5]] è di tipo [[num]], cioè una lista di liste di numeri. I costanti di tipo string sono di tipo [char], infatti, una string come "Ciao" non è altro che una forma abbreviata di scrivere ['C','i','a','o'].

Se T1 fino a Tn sono dei tipi, allora (T1,...,Tn) è il tipo delle tupli che abbiamo degli oggetti di questi tipi come componenti. Per esempio, (True,"ciao",36) è di tipo (bool,[char],num).

Se T1 e T2 sono dei tipo, allora T!->T2 è il tipo di una funzione com parametri in T1 e risultati in T2. Per esempio, la funzione sum è di tipo [num]->num. La funzione quadrisol, data sopra, è di tipo num->num->num->[num]. Nota che "->" si associa a destra.

Gli script di Miranda possono includere delle dichiarazioni dei tipi. Queste si scrivono con "::" col significato "è di tipo". Esempio

        sq :: num -> num
        sq n = n * n

La dichiarazione del tipo non è necessaria, comunque. Il compilatore è sempre in grado di dedurre il tipo di un identificatore dall'equazione che lo definisce. Spesso, gli script in Miranda contengono delle dichiarazioni di tipo poiché sono utili per motivi di documentazione (e forniscono un'ulteriore controllo poiché il controllore dei tipi si lamenterà se il tipo dichiarato è diverso di quello dedotto).

I tipi possono essere polimorfici, nel senso di Milner [Milner 78]. Questo si indica con i simboli * ** *** ecc. come alfabeto di variabili di tipo generico. Ad esempio, la funzione d'identità, definita nella libraria di Miranda come:

        id x = x
è del seguente tipo
        id :: * -> *
Questo significa che la funzione d'identità è di molti tipi, specificatamente, tutti quelli che si possono ottenere sostituendo un tipo arbitrario per il variabile di tipo generico, p.es. "num->num", "bool->bool", "(*->**) -> (*->**)" e così via.

Illustriamo il sistema di tipi di Miranda dando i tipi di alcune delle funzioni fin qui definite in quest'articolo.

        fatt :: num -> num
        ack :: num -> num -> num
        sum :: [num] -> num
        mese :: [char] -> bool
        riversa :: [*] -> [*]
        fst :: (*,**) -> *
        snd :: (*,**) -> **
        foldr :: (*->**->**) -> [*] -> **
        permut :: [*] -> [[*]]

I Tipi Definiti dall'Utente

L'utente può introdurre dei tipi nuovi. Questo si fa con un'equazione in "::=". Per esempio, si potrebbe inrodurre un tipo per gli alberi binari con etichetta come segue,

        albero ::= Nila | Nodo num tree tree
Questo introduce tre nuovi identificatori - "albero", che è il nome del tipo, e "Nila" e "Nodo" che sono i costruttori di alberi. Nila è un costruttore atomico, mentre Nodo prende tre parametri, dei tipi mostrati. Ecco un esempio di un albero costruito con questi costruttori
        t1 = Nodo 7 (Nodo 3 Nila Nila) (Nodo 4 Nila Nila)

Per analizzare un oggetto di un tipo definito dall'utente, usiamo il "pattern matching". Per esempio, ecco una definizione di una funzione che rende l'immagine di un albero riflesso allo specchio

        rispecchia Nila = Nila
        rispecchia (Nodo a x y) = Nodo a (rispecchia y) (rispecchia x)

I tipi definiti dall'utente possono essere polimorfici - questo si specifica introducendo una o più variabili di tipo generico come parametri dell'equazione "::=". Ad esempio, possiamo generalizzare la definizione di "albero" per ammettere delle etichette arbitrari, così:

        albero * ::= Nila | Nodo * (albero *) (albero *)
che introduce una famiglia di tipi di alberi, compresi albero num, albero bool, albero (char->char), e così via.

I tipi introdotti dalle definizioni "::=" si chiamano "tipi algebraici". I tipi algebraici sono un'idea molto generale. Comprendono i tipi scalare enumerati, p.es.

        colore ::= Rosso | Arancione | Giallo | Verbe | Azzurro | Violetta
ed in pi+ ci dànno un modo di fare i tipi unione, per esempio
        bool_o_num ::= Sinistra bool | Destra num

È interessante notare che tutti i tipi basilari di Miranda si potrebbero definire partendo dai primi principi, attraverso le equazioni "::=". Ad esempio, ecco le definizioni di bool, i numeri (naturali) e le liste,

        bool ::= True | False
        nat ::= Zero | Suc nat
        lista * ::= Nil | Cons * (lista *)
I tipi come "num" sono integrati per motivi di efficienza - non è una necessità logica.

Type Synonyms

Il programatore in Miranda può introdurre un nome nuovo per un tipo già esistente. Usiamo "==" per queste definizioni, per distinguerle dalle ordinarie definizioni di valore. Esempi

        stringa == [char]
        matrice == [[num]]

I sinonimi di tipo sono del tutto trasparente al coontrollore dei tipo - è meglio pensare a loro come dei macro. È possibile inoltre introdurre dei sinonimi per delle famiglie di tipi. Questo si fa utilizzando i simboli dei tipi generici come parametri formali, come in

        array * == [[*]]
ed ora, ad.es., `array num' è dello stesso tipo di `matrice'.

I Tipi di Dati Astratti

Oltre ai tipi di dati concreti, introdotti dalle equazioni "::=" o "==", Miranda premette la definizione di tipi astratti, di cui l'implementazione è "nascosta" dal resto del programma. Per far vedere come questo funzione, diamo l'esempio standard della definizione di una pila come tipo astratto. (qui basato sulle liste):

        abstype pila *
        with  vuota :: pila *
              e_vuota :: pila * -> bool
              push :: * -> pila * -> pila *
              pop :: pila * -> pila *
              top :: pila * -> *

        pila * == [*]
        vuota = []
        e_vuota x = (x=[])
        push a x = (a:x)
        pop (a:x) = x
        top (a:x) = a

Vediamo che la definizione di un tipo astratto consiste in due parti. Per prima una dichiarazione della forma "abstype ... with ...", in cui i nomi che seguono la "with" si chiamano la "firma" del tipo astratto. Questi nomi sono l'interfaccia fra il tipo astratto e il resto del programma. In seguito, un'insieme di equazioni dànno delle legature ("bindings") per i nomi introdotti nella dichiarazione abstype. Queste si chiamano le equazioni d'implementazione.

L'astrazione del tipo viene forzato dal controllore dei tipi. Il meccanismo funziona come segue: mentre controlla le equazioni d'implementazione, il tipo astratto e la sua rappresentazione vengono tratti come se fossero dello stesso tipo. In tutto il resto dello script, il tipo astratto e la sua rappresentazione vengono trattati come due tipi diversi e per niente correlati. Questo è alquanto diverso del solito meccanismo per implementare i tipi astratti, ma porta vari vantaggi e viene discusso più a lungo in [Turner 85].

La Compilazione Separata e il Linkaggio

I meccanismi di base per la compilazione separata e il linkaggio sono estremamente semplici. Qualunque script in Miranda può contenere delle direttive della forma:

        %include "pathname"
dove "pathname" è il nome di un'altro archivio script Miranda (che potrebbe a sua volta contenere delle direttive "include", e così via ricorsivamente - dei cicli nella struttura degli include non sono permessi, comunque). La visibilità dei nomi allo script che fa l'include viene controllata da una direttiva nello script incluso, della forma
        %export nomi
È permesso esportare dei tipi oltre che dei valori. Non è consentita l'esportazione di un valore in un luogo dove il suo tipo è sconosciuto, dunque qualora si esporta un'oggetto di un tipo definito localmente, bisogna esportare anche il nome del suo tipo. L'esportazione del nome di un tipo "::=" esporta automaticamente tutti i suoi costruttori. Se uno script non contiene alcuna direttiva export, il comportamento di difetto è che tutti i nomi (e tuti i nomi dei tipi) ivi definiti saranno esportati (ma non quelli che acquistò attraverso delle direttive %include).

È consentito, inoltre, scrivere uno script parametrizzato, in cui certi nomi e/o nomi dei tipi sono dichiarati "free" ("liberi"). Un esempio sarebbe quando vogliamo scrivere un pacchetto per fare l'algebra sulle matrici senza sapere di che tipo saranno gli elementi delle matrici. L'intestazione per un tale pacchetto potrebbe avere il seguente aspetto:

        %free { elemento :: type
                zero, unita :: elemento
                mult, add, sott, divide :: element->element->element
              }

        %export matmult determinante eigenvalori eigenvettori ...
        || qui seguirebbero le defiinizioni di matmult, determinante,
        || eigenvalori, ecc. in termini degli identificatori liberi
        || zero, unita, mult, add, sott, divide

Nello script che lo utilizza, la corrispondente direttiva %include deve dare un'insieme di "bindings" per i variabili liberi dello script incluso. Per esempio, ecco un'istanziazione del pacchetto per matrici delineato sopra, con i numeri reali come tipo scelto per gli elementi:

        %include "matrix_pack"
                 { elemento == num; zero = 0; unita = 1
                   mult = *; add = +; sott = -; divide = /
                 }

Le tre direttive %include, %export e %free forniscono al programmatore in Miranda di un meccanismo flessibile e sicuro nei tipi per la strutturazione di pezzi di software di grandi dimensioni, partendo da librerie di componenti più piccoli.

La compilazione separata viene gestita senza l'intervento dell'utente. Ogni archivio contenente uno script in Miranda ha come ombra un archivio di codice oggetto creato dal sistema, e gli archivi di codice oggetti vengono automaticamente ricreati e rilinkati se scadono rispetto alle loro sorgenti. (Questo comportamento è fortemente analogo a quello ottenuto dal programma "make" di UNIX, tranne che all'utente non viene richiesto di scrivere una makefile - le informazioni necessari sulle dipendenze vengono inferiti dalle direttive %include nelle sorgenti in Miranda).

Lo Stato Attuale dell'Implementazione

Un'implementazioni di Miranda è disponibili per VAX, SUN-3, SUN-4, DECstation, MIPS, Apollo, e varie altre macchine che girano Berkeley UNIX, e anche per la serie HP9000 sotto system 5. Si tratta di un'implementazione interpretiva che funziona compilando gli script di Miranda in un codice intermediario basato sui combinatori. Attualmente è installato a 400 siti (a gennaio 1990). Ulteriori informazioni sulle licenze d'uso si possono avere dagli indirizzi di rete:
(INTERNET:) "mira-request@ukc.ac.uk"
(UUCP:) "mcvax!ukc!mira-request" o per posta tradizionale da


    Research Software Ltd
    23 St Augustines Road
    Canterbury
    Kent CT1 1XP
    England
    (phone +44 227 471844)

Si prevede la trasportazione su altri tipi di macchina nel prossimo futuro, e si sta studiando (da apparire con tempi più lunghi) la possibilità di fare dei compilatori in codice nativo per Miranda su un numero di macchine, per fornire un'implementazione molto più veloce.

Referenze

Milner, R. "A Theory of Type Polymorphism in Programming" Journal of Computer and System Sciences, vol 17, 1978.

Thompson, S.J. "Laws in Miranda" Proceedings 4th ACM International Conference on LISP and Functional Programming, Boston Mass, August 1986.

Turner, D.A. "Miranda: A non-strict functional language with polymorphic types" Proceedings IFIP International Conference on Functional Programming Languages and Computer Architecture, Nancy France, September 1985 (Springer Lecture Notes in Computer Science, vol 201).

[Note - questa occhiata a Miranda apparse per primo in SIGPLAN Notices, Dicembre 1986. È stata leggermente modificata per aggiornarla.