API e programmazione funzionale


15

Dalla mia (ammessa limitata) esposizione a linguaggi di programmazione funzionale, come Clojure, sembra che l'incapsulamento dei dati abbia un ruolo meno importante. Di solito vari tipi nativi come mappe o set sono la valuta preferita per rappresentare i dati, rispetto agli oggetti. Inoltre, tali dati sono generalmente immutabili.

Ad esempio, ecco una delle citazioni più famose della fama di Rich Hickey of Clojure, in un'intervista sull'argomento :

Fogus: seguendo questa idea, alcune persone sono sorprese dal fatto che Clojure non si cimenta nell'incapsulamento dei dati sui suoi tipi. Perché hai deciso di rinunciare a nascondere i dati?

Hickey: chiariamo che Clojure enfatizza fortemente la programmazione alle astrazioni. Ad un certo punto, però, qualcuno dovrà avere accesso ai dati. E se hai una nozione di "privato", hai bisogno delle corrispondenti nozioni di privilegio e fiducia. E ciò aggiunge una tonnellata di complessità e poco valore, crea rigidità in un sistema e spesso costringe le cose a vivere in posti dove non dovrebbero. Ciò si aggiunge alle altre perdite che si verificano quando le informazioni semplici vengono inserite in classi. Nella misura in cui i dati sono immutabili, ci sono pochi danni che possono derivare dalla fornitura dell'accesso, oltre al fatto che qualcuno potrebbe arrivare a dipendere da qualcosa che potrebbe cambiare. Bene, okay, le persone lo fanno sempre nella vita reale e quando le cose cambiano si adattano. E se sono razionali, sanno quando prendono una decisione in base a qualcosa che può cambiare e che in futuro potrebbero aver bisogno di adattarsi. Quindi, è una decisione di gestione del rischio, una che penso che i programmatori dovrebbero essere liberi di prendere. Se le persone non hanno la sensibilità per desiderare di programmare le astrazioni e diffidare dal sposare i dettagli dell'implementazione, allora non saranno mai buoni programmatori.

Proveniente dal mondo OO, questo sembra complicare alcuni dei principi sanciti che ho imparato negli anni. Questi includono Information Hiding, Law of Demeter e Uniform Access Principle, per citarne alcuni. Il filo conduttore è che l'incapsulamento ci consente di definire un'API affinché gli altri sappiano cosa dovrebbero e non dovrebbero toccare. In sostanza, la creazione di un contratto che consente al manutentore di alcuni codici di apportare liberamente modifiche e refactoring senza preoccuparsi di come introdurre bug nel codice del consumatore (principio Open / Closed). Fornisce inoltre un'interfaccia pulita e curata per gli altri programmatori per sapere quali strumenti possono utilizzare per ottenere o basarsi su tali dati.

Quando è possibile accedere direttamente ai dati, il contratto API viene interrotto e tutti i vantaggi dell'incapsulamento sembrano scomparire. Inoltre, i dati strettamente immutabili sembrano rendere molto meno utile il passaggio di strutture specifiche del dominio (oggetti, strutture, record) nel senso di rappresentare uno stato e l'insieme di azioni che possono essere eseguite su quello stato.

In che modo le basi di codici funzionali affrontano questi problemi che sembrano sorgere quando le dimensioni di una base di codice aumentano in modo tale che le API devono essere definite e molti sviluppatori sono coinvolti nel lavorare con parti specifiche del sistema? Sono disponibili esempi di questa situazione che dimostrano come questo viene gestito in questo tipo di basi di codice?


2
È possibile definire un'interfaccia formale senza la nozione di oggetti. Basta creare la funzione dell'interfaccia documentandoli. Non fornire documentazione per i dettagli di implementazione. Hai appena creato un'interfaccia.
Scara95,

@ Scara95 Questo non significa che devo lavorare per implementare il codice per un'interfaccia e scrivere abbastanza documentazione su di esso per avvertire il consumatore cosa fare e cosa non fare? Cosa succede se il codice cambia e la documentazione diventa obsoleta? Per questo motivo preferisco generalmente il codice di auto-documentazione.
jameslk,

Devi comunque documentare l'interfaccia.
Scara95,

3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.Non proprio. L'unica cosa che cambia è che le modifiche finiscono su un nuovo oggetto. Questa è una grande vittoria quando si tratta di ragionare sul codice; passare oggetti mutabili significa dover tenere traccia di chi potrebbe mutarli, un problema che si ingrandisce con la dimensione del codice.
Doval,

Risposte:


10

Prima di tutto, vado a commentare i commenti di Sebastian su ciò che è funzionale, che cos'è la digitazione dinamica. Più in generale, Clojure è un sapore del linguaggio funzionale e della comunità e non dovresti generalizzare troppo basandoti su di esso. Farò alcune osservazioni da più di una prospettiva ML / Haskell.

Come menziona Basile, il concetto di controllo degli accessi esiste in ML / Haskell e viene spesso utilizzato. Il "factoring" è leggermente diverso dai linguaggi OOP convenzionali; in OOP il concetto di classe svolge contemporaneamente il ruolo di tipo e modulo , mentre i linguaggi funzionali (e procedurali tradizionali) li trattano ortogonalmente.

Un altro punto è che ML / Haskell sono molto pesanti sui generici con la cancellazione del tipo e che questo può essere usato per fornire un diverso sapore di "occultamento delle informazioni" rispetto all'incapsulamento OOP. Quando un componente conosce solo il tipo di un elemento di dati come parametro di tipo, a quel componente possono essere trasmessi in modo sicuro valori di quel tipo, e tuttavia gli sarà impedito di fare molto con loro perché non conosce e non può conoscere il loro tipo concreto (non esiste un instanceofcast universale o di runtime in queste lingue). Questo post sul blog è uno dei miei esempi introduttivi preferiti a queste tecniche.

Avanti: nel mondo FP è molto comune usare strutture di dati trasparenti come interfacce per componenti opachi / incapsulati. Ad esempio, i modelli di interpreti sono molto comuni in FP, in cui le strutture di dati vengono utilizzate come alberi di sintassi che descrivono la logica e vengono inviate al codice che le "esegue". Lo stato, correttamente detto, esiste quindi in modo effimero quando viene eseguito l'interprete che consuma le strutture di dati. Anche l'implementazione dell'interprete può cambiare purché comunichi ancora con i clienti in termini degli stessi tipi di dati.

Ultimo e più lungo: l'incapsulamento / nascondere informazioni è una tecnica , non una fine. Pensiamo un po 'a ciò che fornisce. L'incapsulamento è una tecnica per riconciliare il contratto e l' implementazione di un'unità software. La situazione tipica è questa: l'implementazione del sistema ammette valori o afferma che, secondo il suo contratto, non dovrebbe esistere.

Una volta che lo guardi in questo modo, possiamo sottolineare che FP fornisce, oltre all'incapsulamento, una serie di strumenti aggiuntivi che possono essere utilizzati allo stesso fine:

  1. Immutabilità come default pervasivo. È possibile passare valori di dati trasparenti a codice di terze parti. Non possono modificarli e metterli in stati non validi. (La risposta di Karl sottolinea questo punto.)
  2. Sistemi di tipo sofisticati con tipi di dati algebrici che consentono di controllare con precisione la struttura dei tipi, senza scrivere molto codice. Usando giudiziosamente queste strutture è spesso possibile progettare tipi in cui "cattivi stati" sono semplicemente impossibili. (Slogan: "Rendi non rappresentabili gli stati illegali." ) Invece di usare l'incapsulamento per controllare indirettamente l'insieme degli stati ammissibili di una classe, preferirei semplicemente dire al compilatore quali sono quelli e me li garantisco!
  3. Modello dell'interprete, come già menzionato. Una chiave per progettare un buon tipo di albero di sintassi astratto è:
    • Prova a progettare il tipo di dati dell'albero di sintassi astratto in modo che tutti i valori siano "validi".
    • In caso contrario, fare in modo che l'interprete rilevi esplicitamente combinazioni non valide e le rifiuti in modo pulito.

Questa serie di F # "Progettare con i tipi" rende abbastanza decente la lettura di alcuni di questi argomenti, in particolare il n. 2. (È da dove proviene il link "rendere gli stati illegali non rappresentabili" dall'alto.) Se guardi da vicino, noterai che nella seconda parte dimostrano come usare l'incapsulamento per nascondere i costruttori e impedire ai clienti di costruire istanze non valide. Come ho detto sopra, fa parte del toolkit!


9

Non riesco davvero a sopravvalutare il grado in cui la mutabilità causa problemi nel software. Molte delle pratiche che vengono battute nella nostra testa compensano i problemi causati dalla mutabilità. Quando togli la mutabilità, non hai più bisogno di quelle pratiche.

Quando hai l'immutabilità, sai che la struttura dei tuoi dati non cambierà inaspettatamente da sotto di te durante il runtime, quindi puoi creare le tue strutture di dati derivate per il tuo uso mentre aggiungi funzionalità al tuo programma. La struttura di dati originale non ha bisogno di sapere nulla su queste strutture di dati derivati.

Ciò significa che le strutture dei dati di base tendono ad essere estremamente stabili. Le nuove strutture di dati vengono derivate da esso attorno ai bordi, se necessario. È davvero difficile da spiegare fino a quando non hai fatto un programma funzionale significativo. Ti preoccupi sempre meno della privacy e pensi sempre più alla creazione di strutture di dati pubblici generici durevoli.


Una cosa che vorrei aggiungere è che la variabile immutabile fa sì che i programmatori si attacchino alla struttura di dati distribuiti e sparsi, se esiste una struttura. Tutti i dati sono strutturati per creare un gruppo logico, per una facile individuazione e attraversamento, non per il trasporto. Questa è una progressione logica che farai dopo aver fatto abbastanza programmazione funzionale.
Xephon

8

La tendenza di Clojure a usare solo hash e primitivi non è, secondo me, parte del suo patrimonio funzionale, ma parte del suo patrimonio dinamico. Ho visto tendenze simili in Python e Ruby (entrambi orientati agli oggetti, imperativi e dinamici, anche se entrambi hanno un supporto abbastanza buono per le funzioni di ordine superiore), ma non in, diciamo, Haskell (che è tipicamente statico, ma puramente funzionale , con costrutti speciali necessari per sfuggire all'immutabilità).

Quindi la domanda che devi porre non è: in che modo i linguaggi funzionali gestiscono le grandi API, ma come lo fanno i linguaggi dinamici. La risposta è: buona documentazione e un sacco di test unitari. Fortunatamente, i linguaggi dinamici moderni di solito hanno un ottimo supporto per entrambi; ad esempio, sia Python che Clojure hanno un modo per incorporare la documentazione nel codice stesso, non solo i commenti.


Su linguaggi tipicamente statici, (puramente) funzionali, non esiste un modo (semplice) per portare una funzione con un tipo di dati come nella programmazione OO. Quindi la documentazione conta comunque. Il punto è che non è necessario il supporto linguistico per definire un'interfaccia.
Scara95,

5
@ Scara95 Puoi elaborare cosa intendi per "trasportare una funzione con un tipo di dati"?
Sebastian Redl,

6

Alcuni linguaggi funzionali offrono la possibilità di incapsulare o nascondere i dettagli di implementazione in tipi di dati e moduli astratti .

Ad esempio, OCaml ha moduli definiti da una raccolta di tipi e valori astratti denominati (in particolare funzioni che operano su questi tipi astratti). Quindi, in un certo senso, i moduli di Ocaml stanno reimpostando le API. Ocaml ha anche dei funzioni, che stanno trasformando alcuni moduli in un altro, fornendo così una programmazione generica. Quindi i moduli sono compositivi.

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.