In che modo i linguaggi puramente funzionali gestiscono la modularità?


23

Vengo da uno sfondo orientato agli oggetti in cui ho imparato che le classi sono o almeno possono essere utilizzate per creare uno strato di astrazione che consenta un facile riciclaggio del codice che può quindi essere utilizzato per creare oggetti o essere usato in eredità.

Come ad esempio posso avere una classe di animali e quindi ereditare cani e gatti e tali da ereditare tutti gli stessi tratti, e da quelle sottoclassi posso quindi creare oggetti che possono specificare una razza di animali o persino il nome di esso.
Oppure posso usare le classi per specificare più istanze dello stesso codice che gestisce o contiene cose leggermente diverse; come nodi in un albero di ricerca o connessioni di database diverse e cosa no.

Di recente mi sto muovendo verso la programmazione funzionale, quindi stavo iniziando a chiedermi:
come fanno i linguaggi puramente funzionali a gestire cose del genere? Cioè, le lingue senza alcun concetto di classi e oggetti.


1
Perché pensi che funzionale non significhi classi? L'alcune delle classi di frist provenivano da LISP - CLOS Guardate Clojure spazi dei nomi e dei tipi o moduli e Haskell .

Ho fatto riferimento ai linguaggi funzionali che NON hanno lezioni, sono ben consapevole dei pochi che DO
Electric Coffee,

1
Caml come esempio, il suo linguaggio gemello OCaml aggiunge oggetti, ma Caml non li possiede.
Caffè elettrico

12
Il termine "puramente funzionale" si riferisce a linguaggi funzionali che mantengono la trasparenza referenziale e non è correlato al fatto che la lingua abbia o meno caratteristiche orientate agli oggetti.
sepp2k,

2
La torta è una bugia, il riutilizzo del codice in OO è molto più difficile che in FP. Nonostante tutto ciò che OO ha richiesto il riutilizzo del codice nel corso degli anni, l'ho visto seguire almeno un paio di volte. (sentiti libero di dire che devo fare qualcosa di sbagliato, mi sento a mio agio con quanto scrivo il codice OO dopo aver progettato e mantenuto sistemi OO per anni, conosco la qualità dei miei risultati)
Jimmy Hoffa

Risposte:


19

Molti linguaggi funzionali hanno un sistema di moduli. (A proposito anche molti linguaggi orientati agli oggetti.) Ma anche in assenza di uno, puoi usare le funzioni come moduli.

JavaScript è un buon esempio. In JavaScript, le funzioni sono utilizzate sia per implementare moduli sia per incapsulamento orientato agli oggetti. In Scheme, che è stata la principale fonte d'ispirazione per JavaScript, ci sono solo funzioni. Le funzioni sono utilizzate per implementare quasi tutto: oggetti, moduli (chiamati unità in Racket), persino strutture di dati.

OTOH, Haskell e la famiglia ML hanno un sistema di moduli esplicito.

L'orientamento agli oggetti riguarda l'astrazione dei dati. Questo è tutto. Modularità, ereditarietà, polimorfismo, persino lo stato mutabile sono preoccupazioni ortogonali.


8
Puoi spiegare come funzionano queste cose in modo un po 'più dettagliato in relazione a oop? Invece di affermare semplicemente che i concetti esistono ...
Electric Coffee,

Sidenote - I moduli e le unità sono due diversi costrutti in Racket - i moduli sono paragonabili agli spazi dei nomi e le unità sono una sorta di metà strada tra gli spazi dei nomi e le interfacce OO. I documenti vanno molto più in dettaglio sulle differenze
Jack,

@Jack: non sapevo che anche Racket avesse un concetto chiamato module. Penso che sia un peccato che Racket abbia un concetto chiamato moduleche non è un modulo, e un concetto che è un modulo ma non chiamato module. Ad ogni modo, hai scritto: "le unità sono una sorta di metà tra gli spazi dei nomi e le interfacce OO". Bene, non è quel tipo di definizione di cosa sia un modulo?
Jörg W Mittag,

I moduli e le unità sono entrambi gruppi di nomi associati a valori. I moduli possono avere dipendenze da altri insiemi specifici di associazioni, mentre le unità possono avere dipendenze da alcune serie generali di associazioni che qualsiasi altro codice che utilizza l'unità deve fornire. Le unità sono parametrizzate su associazioni, i moduli no. Un modulo dipendente da un'associazione mape un'unità dipendente da un'associazione mapsono diversi in quanto il modulo deve fare riferimento a qualche mapassociazione specifica , come quella di racket/base, mentre diversi utenti dell'unità possono fornire definizioni diverse mapdell'unità.
Jack,

4

Sembra che tu stia ponendo due domande: "Come puoi ottenere la modularità nei linguaggi funzionali?" che è stato trattato in altre risposte e "come si possono creare astrazioni in linguaggi funzionali?" a cui risponderò.

Nelle lingue OO, tendi a concentrarti sul sostantivo, "un animale", "il server di posta", "la sua forchetta da giardino", ecc. Le lingue funzionali, al contrario, sottolineano il verbo "camminare", "recuperare la posta" , "produrre", ecc.

Non sorprende quindi che le astrazioni nei linguaggi funzionali tendano ai verbi o alle operazioni piuttosto che alle cose. Un esempio che cerco sempre quando cerco di spiegare questo è l'analisi. Nei linguaggi funzionali, un buon modo per scrivere parser è specificare una grammatica e quindi interpretarla. L'interprete crea un'astrazione sul processo di analisi.

Un altro esempio concreto di questo è un progetto a cui stavo lavorando non molto tempo fa. Stavo scrivendo un database in Haskell. Avevo un "linguaggio incorporato" per specificare le operazioni al livello più basso; ad esempio, mi ha permesso di scrivere e leggere cose dal supporto di memorizzazione. Avevo un altro "linguaggio incorporato" separato per specificare le operazioni ai massimi livelli. Poi ho avuto, che è essenzialmente un interprete, per convertire le operazioni dal livello superiore a quello inferiore.

Questa è una forma straordinariamente generale di astrazione, ma non è l'unica disponibile nei linguaggi funzionali.


4

Sebbene la "programmazione funzionale" non abbia implicazioni di vasta portata per questioni di modularità, linguaggi particolari affrontano la programmazione in modo ampio in modi diversi. Il riutilizzo e l'astrazione del codice interagiscono in quanto meno esponi, più è difficile riutilizzare il codice. Mettendo da parte l'astrazione, affronterò due questioni di riusabilità.

I linguaggi OOP tipicamente statici utilizzavano tradizionalmente il sottotipo nominale, il che significa che un codice progettato per classe / modulo / interfaccia A può trattare solo con classe / modulo / interfaccia B quando B menziona esplicitamente A. I linguaggi della famiglia di programmazione funzionale utilizzano principalmente il sottotipo strutturale, il che significa quel codice progettato per A può gestire B ogni volta che B ha tutti i metodi e / o campi di A. B avrebbe potuto essere creato da un team diverso prima che fosse necessaria una classe / interfaccia più generale A. Ad esempio in OCaml, sottotipizzazione strutturale si applica al sistema dei moduli, al sistema a oggetti simile a OOP e ai suoi tipi di varianti polimorfiche piuttosto singolari.

La differenza più evidente tra OOP e FP wrt. la modularità è che l '"unità" predefinita in OOP raggruppa insieme come oggetto varie operazioni sullo stesso caso di valori, mentre l' "unità" predefinita in FP raggruppa insieme come funzione la stessa operazione per vari casi di valori. In FP è ancora molto semplice raggruppare insieme le operazioni, ad esempio come moduli. (A proposito, né Haskell né F # hanno un sistema di moduli della famiglia ML a tutti gli effetti.) Il problema dell'espressioneè il compito di aggiungere in modo incrementale sia le nuove operazioni che lavorano su tutti i valori (ad esempio associando un nuovo metodo agli oggetti esistenti), sia i nuovi casi di valori che tutte le operazioni dovrebbero supportare (ad esempio aggiungendo una nuova classe con la stessa interfaccia). Come discusso nella prima lezione di Ralf Laemmel di seguito (che contiene numerosi esempi in C #), l'aggiunta di nuove operazioni è problematica nei linguaggi OOP.

La combinazione di OOP e FP in Scala potrebbe renderlo una delle lingue più potenti. modularità. Ma OCaml è ancora la mia lingua preferita e secondo la mia opinione personale e soggettiva non manca di Scala. Le due lezioni di Ralf Laemmel che seguono illustrano la soluzione al problema dell'espressione in Haskell. Penso che questa soluzione, sebbene perfettamente funzionante, renda difficile usare i dati risultanti con polimorfismo parametrico. Risolvere il problema dell'espressione con varianti polimorfiche in OCaml, spiegato nell'articolo di Jaques Garrigue collegato di seguito, non ha questo difetto. Collego anche a capitoli di libri di testo che confrontano gli usi della modularità non OOP e OOP in OCaml.

Di seguito sono riportati i collegamenti specifici di Haskell e OCaml che si espandono sul problema dell'espressione :


2
ti dispiacerebbe spiegare di più su ciò che fanno queste risorse e perché le consigli a rispondere alla domanda posta? Le "risposte solo link" non sono del tutto benvenute allo Stack Exchange
moscerino

2
Ho appena fornito una risposta effettiva piuttosto che solo collegamenti, come una modifica.
lukstafi,

0

In realtà, il codice OO è molto meno riutilizzabile, e questo è di progettazione. L'idea alla base di OOP è limitare le operazioni su particolari parti di dati a un determinato codice privilegiato che si trova nella classe o nella posizione appropriata nella gerarchia dell'ereditarietà. Ciò limita gli effetti avversi della mutabilità. Se una struttura di dati cambia, ci sono solo così tante posizioni nel codice che possono essere responsabili.

Con l'immutabilità, non ti importa chi può operare su una data struttura di dati, perché nessuno può modificare la tua copia dei dati. Ciò rende molto più semplice la creazione di nuove funzioni per lavorare su strutture di dati esistenti. Basta creare le funzioni e raggrupparle in moduli che sembrano appropriati dal punto di vista del dominio. Non devi preoccuparti di dove inserirli nella gerarchia dell'eredità.

L'altro tipo di riutilizzo del codice sta creando nuove strutture di dati per lavorare su funzioni esistenti. Questo è gestito in linguaggi funzionali usando funzionalità come generici e classi di tipi. Ad esempio, la classe di tipi Ord di Haskell consente di utilizzare la sortfunzione su qualsiasi tipo con Ordun'istanza. Le istanze sono facili da creare se non esistono già.

Fai il tuo Animalesempio e considera di implementare una funzione di alimentazione. L'implementazione OOP semplice è quella di mantenere una raccolta di Animaloggetti e di eseguirne il ciclo tutti, chiamando il feedmetodo su ciascuno di essi.

Tuttavia, le cose si complicano quando si arriva ai dettagli. Un Animaloggetto naturalmente sa che tipo di cibo mangia e di quanto ha bisogno per sentirsi pieno. Essa non naturalmente sa dove il cibo è conservato e quanto è disponibile, quindi un FoodStoreoggetto è appena diventato una dipendenza di ogni Animal, sia come campo Animaloggetto o passato come parametro del feedmetodo. In alternativa, per mantenere la Animalclasse di più coesa, si potrebbe spostare feed(animal)l' FoodStoreoggetto, o si potrebbe creare un abominio di una classe chiamato AnimalFeedero qualcosa del genere.

In FP, non vi è alcuna inclinazione per i campi di Animalrimanere sempre raggruppati, il che ha alcune interessanti implicazioni per la riusabilità. Diciamo che avere una lista di Animalrecord, con campi come name, species, location, food type, food amount, ecc Hai anche un elenco di FoodStorerecord con campi come location, food typee food amount.

Il primo passo per l'alimentazione potrebbe essere quello di mappare ciascuno di quegli elenchi di record su elenchi di (food amount, food type)coppie, con numeri negativi per le quantità degli animali. È quindi possibile creare funzioni per fare ogni sorta di cose con queste coppie, come sommare le quantità di ciascun tipo di cibo. Queste funzioni non appartengono perfettamente a uno Animalo un FoodStoremodulo, ma sono altamente riutilizzabili da entrambi.

Finisci con un mucchio di funzioni che fanno cose utili [(Num A, Eq B)]riutilizzabili e modulari, ma hai difficoltà a capire dove metterle o come chiamarle come gruppo. L'effetto è che i moduli FP sono più difficili da classificare, ma la classificazione è molto meno importante.


-1

Una delle soluzioni più diffuse è quella di suddividere il codice in moduli, ecco come viene eseguito in JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

L' articolo completo che spiega questo modello in JavaScript , a parte il fatto che esiste un numero di altri modi per definire un modulo, come RequireJS , CommonJS , Google Closure. Un altro esempio è Erlang, in cui sono presenti sia moduli che comportamenti che impongono API e pattern, che svolgono un ruolo simile a quello delle interfacce in OOP.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.