API e programmazione funzionale

15

Dalla mia esposizione (dichiaratamente limitata) ai 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, sopra gli oggetti. Inoltre, tali dati sono generalmente immutabili.

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

Fogus: Following that idea—some people are surprised by the fact that Clojure does not engage in data-hiding encapsulation on its types. Why did you decide to forgo data-hiding?

Hickey: Let’s be clear that Clojure strongly emphasizes programming to abstractions. At some point though, someone is going to need to have access to the data. And if you have a notion of “private”, you need corresponding notions of privilege and trust. And that adds a whole ton of complexity and little value, creates rigidity in a system, and often forces things to live in places they shouldn’t. This is in addition to the other losing that occurs when simple information is put into classes. To the extent the data is immutable, there is little harm that can come of providing access, other than that someone could come to depend upon something that might change. Well, okay, people do that all the time in real life, and when things change, they adapt. And if they are rational, they know when they make a decision based upon something that can change that they might in the future need to adapt. So, it’s a risk management decision, one I think programmers should be free to make. If people don’t have the sensibilities to desire to program to abstractions and to be wary of marrying implementation details, then they are never going to be good programmers.

Venendo dal mondo OO, questo sembra complicare alcuni dei principi sanciti che ho imparato nel corso degli anni. Questi includono l'occultamento dell'informazione, la legge di Demetra e il principio di accesso uniforme, per citarne alcuni. Il filo conduttore è che l'incapsulamento ci consente di definire un'API affinché gli altri sappiano cosa dovrebbero e non devono 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 potrebbe introdurre bug nel codice del consumatore (principio Open / Closed). Fornisce anche un'interfaccia pulita e curata per gli altri programmatori per sapere quali strumenti possono utilizzare per ottenere o costruire su tali dati.

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

In che modo le codebase funzionali affrontano questi problemi che sembrano emergere quando le dimensioni di un codebase diventano enormi, così 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 codebase?

    
posta jameslk 14.12.2015 - 07:25
fonte

4 risposte

10

Prima di tutto, vado a commentare i commenti di Sebastian su cosa sia funzionale, cosa è la digitazione dinamica. Più in generale, Clojure è un sapore di linguaggio e comunità funzionali, 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 una classe riproduce simultaneamente il ruolo di tipo e modulo , mentre i linguaggi funzionali (e procedurali tradizionali) trattano questi ortogonali.

Un altro punto è che ML / Haskell sono molto pesanti con i generici con cancellazione di tipi e che questo può essere usato per fornire un diverso sapore di "nascondiglio dell'informazione" rispetto all'incapsulamento OOP. Quando un componente conosce solo il tipo di un elemento di dati come parametro di tipo, quel componente può essere tranquillamente distribuito con valori di quel tipo, e tuttavia gli sarà impedito di fare molto con loro perché non sa e non può conoscere il loro tipo concreto (non esiste un% universaleinstanceof o una trasmissione in runtime in queste lingue). Questo post di blog è uno dei miei esempi introduttivi preferiti a queste tecniche.

Avanti: nel mondo FP è molto comune utilizzare strutture di dati trasparenti come interfacce per componenti opachi / incapsulati. Ad esempio, i modelli di interprete sono molto comuni in FP, dove le strutture dati sono utilizzate come alberi di sintassi che descrivono la logica e alimentate al codice che le "esegue". Lo stato, propriamente detto, esiste quindi effimero quando l'interprete esegue che consuma le strutture dei dati. Anche l'implementazione dell'interprete può cambiare finché continua a comunicare con i client in termini di stessi tipi di dati.

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

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

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

Questa serie F # "Design with types" rende la lettura piuttosto decente su alcuni di questi argomenti , in particolare # 2. (È da dove viene il collegamento "make illegal states nonpresentable".) Se osservate attentamente, noterete che nella seconda parte dimostrano come usare l'incapsulamento per nascondere i costruttori e impedire ai client di costruire istanze non valide. Come ho detto sopra, è parte del toolkit!

    
risposta data 15.12.2015 - 02:02
fonte
9

Non riesco davvero a sopravvalutare il grado in cui la mutabilità causa problemi nel software. Molte delle pratiche che vengono praticate nelle nostre teste sono in grado di compensare i problemi causati dalla mutabilità. Quando togli la mutabilità, non hai bisogno di quelle pratiche.

Quando hai immutabilità, sai che la tua struttura dati non cambierà da sotto di te in modo imprevisto durante il runtime, così puoi creare le tue strutture dati derivative per tuo uso mentre aggiungi funzionalità al tuo programma. La struttura dati originale non ha bisogno di sapere nulla su queste strutture dati derivative.

Questo significa che le strutture dei dati di base tendono ad essere estremamente stabili. Le nuove strutture di dati si ottengono da esse attorno ai bordi secondo necessità. È davvero difficile da spiegare finché non hai fatto un programma funzionale significativo. Ti ritrovi a occuparti sempre meno della privacy e pensi di creare strutture di dati pubbliche generiche durature sempre di più.

    
risposta data 14.12.2015 - 14:59
fonte
8

La tendenza del Clojure ad usare solo hash e primitive non è, a mio parere, 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, per esempio, Haskell (che è tipizzato staticamente, ma puramente funzionale , con costrutti speciali necessari per evitare l'immutabilità).

Quindi la domanda che devi porre non è, in che modo i linguaggi funzionali gestiscono le grandi API, ma come 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 di incorporare la documentazione nel codice stesso, non solo nei commenti.

    
risposta data 14.12.2015 - 10:59
fonte
6

Alcuni linguaggi funzionali consentono di incapsulare o nascondere i dettagli di implementazione in tipi di dati astratti e moduli

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 ridefinendo le API. Ocaml ha anche dei funtori, che stanno trasformando alcuni moduli in un altro, fornendo così una programmazione generica. Quindi i moduli sono compositivi.

    
risposta data 14.12.2015 - 09:25
fonte