Omettere l'eredità nei linguaggi di programmazione


10

Sto sviluppando il mio linguaggio di programmazione. È un linguaggio generico (pensa a Python tipicamente scritto per il desktop, cioè int x = 1;) non inteso per il cloud.

Pensi che sia giusto non consentire l'eredità o i Mixin? (dato che l'utente avrebbe almeno interfacce)

Ad esempio: Google Go, un linguaggio di sistema che ha scioccato la comunità di programmatori non consentendo l'ereditarietà.

Risposte:


4

In effetti, troviamo sempre di più che l'utilizzo dell'ereditarietà non è ottimale perché (tra gli altri) porta a un accoppiamento stretto.

L'ereditarietà dell'implementazione può sempre essere sostituita dall'ereditarietà dell'interfaccia più la composizione, e progettazioni software più moderne tendono ad andare sempre più nella direzione dell'utilizzo dell'ereditarietà, a favore della composizione.

Quindi , non fornire l'eredità in particolare è una decisione di progettazione completamente valida e molto in voga .

Mixins, d'altra parte, non sono nemmeno (ancora) una caratteristica del linguaggio mainstream, e le lingue che non forniscono una funzione denominata “mixin” capiscono spesso cose molto diverse da esso. Sentiti libero di fornirlo o no. Personalmente lo trovo molto utile, ma implementarlo nel modo giusto potrebbe essere molto difficile.


13

Ragionare sul fatto che l'ereditarietà (o qualsiasi singola caratteristica, davvero) sia necessaria o meno senza considerare anche il resto della semantica della lingua è inutile; stai litigando nel vuoto.

Ciò di cui hai bisogno è una filosofia coerente nella progettazione del linguaggio; la lingua deve essere in grado di risolvere elegantemente i problemi per cui è progettata. Il modello per raggiungere questo obiettivo potrebbe richiedere o meno l'ereditarietà, ma è difficile giudicarlo senza il quadro generale.

Se, ad esempio, il tuo linguaggio ha funzioni di prima classe, applicazione di funzioni parziali, tipi di dati polimorfici, variabili di tipo e tipi generici, hai praticamente coperto le stesse basi che avresti con l'eredità OOP classica, ma usando un paradigma diverso.

Se si dispone di associazione tardiva, tipizzazione dinamica, metodi-come-proprietà, argomenti di funzioni flessibili e funzioni di prima classe, si coprono anche gli stessi motivi, ma ancora una volta, usando un paradigma diverso.

(Trovare esempi per i due paradigmi delineati è lasciato come esercizio per il lettore.)

Quindi, pensa al tipo di semantica che desideri, gioca con loro e vedi se sono sufficienti senza ereditarietà. In caso contrario, potresti decidere di gettare l'eredità nel mix o puoi decidere che manchi qualcos'altro.


9

Sì.

Penso che non consentire l'ereditarietà vada bene, soprattutto se la tua lingua è tipizzata in modo dinamico. È possibile ottenere un riutilizzo di codice simile, ad esempio delegazione o composizione. Ad esempio, ho scritto alcuni programmi moderatamente complicati - va bene, non così complicato:) - in JavaScript senza ereditarietà. Fondamentalmente ho usato oggetti come strutture di dati algebrici con alcune funzioni associate come metodi. Avevo anche un sacco di funzioni che non erano metodi che operavano su questi oggetti.

Se hai una digitazione dinamica - e suppongo che tu lo sia - puoi avere anche il polimorfismo senza ereditarietà. Inoltre, se consenti l'aggiunta di metodi arbitrari agli oggetti in fase di runtime, non avrai davvero bisogno di cose come i mixin.

Un'altra opzione - che ritengo sia una buona scelta - è emulare JavaScript e utilizzare i prototipi. Questi sono più semplici delle classi ma anche molto flessibili. Solo qualcosa da considerare.

Quindi, tutto sommato, ci proverei.


2
Finché esiste un modo ragionevole per eseguire i tipi di attività tipicamente gestite con l'eredità, andare avanti. Potresti provare alcuni casi d'uso, per essere sicuro (forse anche spingendo fino a sollecitare esempi di problemi da altri, per evitare l'auto-pregiudizio ...)
comingstorm

No È staticamente e fortemente tipizzato, lo menzionerò all'inizio.
Christopher

Dato che è tipizzato staticamente, non puoi semplicemente aggiungere metodi casuali agli oggetti in fase di esecuzione. Tuttavia, puoi ancora fare la tipizzazione di anatra: controlla i protocolli Gosu per un esempio. Ho usato i protocolli un po 'durante l'estate ed erano davvero utili. Gosu ha anche "miglioramenti" che sono un modo per aggiungere metodi alle classi dopo il fatto. Potresti considerare di aggiungere anche qualcosa del genere.
Tikhon Jelvis,

2
Per favore, per favore, abbastanza per favore, smetti di relazionare l'eredità e il riutilizzo del codice. È noto da tempo che l'ereditarietà è davvero uno strumento pessimo per il riutilizzo del codice.
Jan Hudec,

3
L'ereditarietà è solo uno strumento per il riutilizzo del codice. Spesso è un ottimo strumento e altre volte è orribile. Preferire la composizione sull'ereditarietà non significa mai usare l'ereditarietà.
Michael K,

8

Sì, è una decisione di progettazione perfettamente ragionevole omettere l'eredità.

Esistono, in effetti, ottime ragioni per rimuovere l'ereditarietà dell'implementazione, in quanto può produrre del codice estremamente complesso e difficile da mantenere. Andrei persino al punto di considerare l'ereditarietà (in quanto è tipicamente implementata nella maggior parte dei linguaggi OOP) come una cattiva funzionalità.

Clojure, ad esempio, non fornisce l'ereditarietà dell'implementazione, preferendo fornire una serie di caratteristiche ortogonali (protocolli, dati, funzioni, macro) che possono essere utilizzate per ottenere gli stessi risultati ma in modo molto più pulito.

Ecco un video che ho trovato molto illuminante su questo argomento generale, in cui Rich Hickey identifica le fonti fondamentali di complessità nei linguaggi di programmazione (inclusa l'ereditarietà) e presenta alternative per ciascuno: Semplice reso facile


In molti casi, l'ereditarietà dell'interfaccia è più appropriata. Ma se usato con moderazione, l'ereditarietà di impianto può consentire la rimozione di un sacco di codice del boilerplate ed è la soluzione più elegante. Richiede solo programmatori più intelligenti e più attenti della scimmia Python / JavaScript media.
Erik Alapää,

4

Quando mi sono imbattuto per la prima volta nel fatto che le classi VB6 non supportano l'ereditarietà (solo interfacce), mi ha davvero infastidito (e lo fa ancora).

Tuttavia, il motivo per cui è così male è che non aveva nemmeno parametri di costruzione, quindi non si poteva fare l'iniezione di dipendenza normale (DI). Se hai DI, questo è più importante dell'eredità perché puoi seguire il principio di favorire la composizione rispetto all'eredità. È comunque un modo migliore per riutilizzare il codice.

Non hai Mixin però? Se vuoi implementare un'interfaccia e delegare tutto il lavoro di quell'interfaccia a un set di oggetti attraverso l'iniezione delle dipendenze, i Mixin sono l'ideale. Altrimenti devi scrivere tutto il codice boilerplate per delegare ogni metodo e / o proprietà all'oggetto figlio. Lo faccio molto (grazie a C #) ed è una cosa che vorrei non dover fare.


Sì, ciò che ha portato a questa domanda è l'implementazione del DOM SVG in cui il riutilizzo del codice è molto utile.
Christopher

2

Per non essere d'accordo con un'altra risposta: no, butti fuori funzionalità quando sono incompatibili con qualcosa che desideri di più. Java (e altri linguaggi GC'd) ha eliminato la gestione esplicita della memoria perché voleva una maggiore sicurezza dei tipi. Haskell ha lanciato la mutazione perché voleva più ragionamenti equazionali e tipi fantasiosi. Persino C ha eliminato (o dichiarato illegale) alcuni tipi di aliasing e altri comportamenti perché desiderava maggiormente le ottimizzazioni del compilatore.

Quindi la domanda è: cosa vuoi di più dell'eredità?


1
Solo la gestione esplicita della memoria e la sicurezza dei tipi sono completamente indipendenti e sicuramente non incompatibili. Lo stesso vale per la mutazione e i "tipi fantasiosi". Sono d'accordo con il ragionamento generale, ma i tuoi esempi sono scelti piuttosto male.
Konrad Rudolph,

@KonradRudolph, gestione della memoria e sicurezza dei tipi sono strettamente correlati. Un'operazione free/ deleteti dà la possibilità di invalidare i riferimenti; a meno che il sistema di tipi non sia in grado di tracciare tutti i riferimenti interessati, ciò rende la lingua non sicura. In particolare, né C né C ++ sono sicuri per i tipi. È vero che puoi scendere a compromessi nell'uno o nell'altro (ad es. Tipi lineari o restrizioni di allocazione) per farli concordare. Per essere precisi, avrei dovuto dire che Java voleva la sicurezza dei tipi con un sistema di tipi particolare e semplice e un'allocazione senza restrizioni .
Ryan Culpepper,

Bene, ciò dipende piuttosto da come si definisce la sicurezza dei tipi. Ad esempio, se si prende la definizione (del tutto ragionevole) di sicurezza del tipo in fase di compilazione e si desidera che l'integrità di riferimento faccia parte del proprio sistema di tipi, anche Java non è sicuro (poiché consente nullriferimenti). Proibire freeè una restrizione aggiuntiva piuttosto arbitraria. In sintesi, se una lingua è sicura per i tipi dipende più dalla tua definizione di sicurezza dei tipi che dalla lingua.
Konrad Rudolph,

1
@Ryan: i puntatori sono perfettamente sicuri per i tipi. Si comportano sempre secondo la loro definizione. Potrebbe non essere come ti piacciono, ma è sempre in base a come sono definiti. Stai cercando di allungarli per essere qualcosa che non sono. I puntatori intelligenti possono garantire la sicurezza della memoria in modo abbastanza banale in C ++.
DeadMG

@KonradRudolph: in Java, ogni Triferimento si riferisce a uno nullo un oggetto di una classe che si estende T; nullè brutto, ma le operazioni nullgenerano un'eccezione ben definita piuttosto che corrompere il tipo invariante sopra. Contrasto con C ++: dopo una chiamata a delete, un T*puntatore può puntare alla memoria che non contiene più un Toggetto. Peggio ancora, facendo un'assegnazione di campo usando quel puntatore in un'assegnazione, potresti aggiornare completamente un campo di un oggetto di una classe diversa se si trovasse a un indirizzo vicino. Questa non è sicurezza del tipo da nessuna definizione utile del termine.
Ryan Culpepper,

1

No.

Se si desidera rimuovere una funzionalità del linguaggio di base, si dichiara universalmente che non è mai, mai necessario (o ingiustificatamente difficile da implementare, che non si applica qui). E "mai" è una parola forte nell'ingegneria del software. Avresti bisogno di una giustificazione molto potente per fare una simile affermazione.


8
Non è vero: l'intero progetto di Java riguardava la rimozione di funzionalità come il sovraccarico dell'operatore, l'ereditarietà multipla e la gestione manuale della memoria. Inoltre, penso che trattare qualsiasi caratteristica del linguaggio come "sacro" sia sbagliato; invece di giustificare la rimozione di una funzione, dovresti giustificare l' aggiunta - non riesco a pensare a nessuna caratteristica che ogni lingua deve avere.
Tikhon Jelvis,

6
@TikhonJelvis: Ecco perché Java è un linguaggio terribile , e non manca di nulla tranne "USARE L'EREDITÀ RACCOLTA DI RIFIUTI" è il motivo numero uno per cui. Le caratteristiche del linguaggio dovrebbero essere giustificate, sono d'accordo, ma questa è una funzionalità del linguaggio di base: ha molte applicazioni utili e non può essere replicata dal programmatore senza violare DRY.
DeadMG

1
@DeadMG Eeeek !!! Linguaggio abbastanza forte.
Christopher

@DeadMG: ho iniziato a scrivere una confutazione come commento, ma poi l'ho trasformata in una risposta (qv).
Ryan Culpepper,

1
"La perfezione si ottiene non quando non è rimasto altro da aggiungere ma quando non è rimasto nulla da rimuovere" (q)
9000

1

Parlando da una prospettiva strettamente C ++, l'ereditarietà è utile per due scopi principali:

  1. Consente la risusabilità del codice
  2. In combinazione con l'override in una classe figlio, consente di utilizzare un puntatore della classe base per gestire un oggetto classe figlio senza conoscerne il tipo.

Per il punto 1 fintanto che hai un modo per condividere il codice senza dover indulgere in troppe acrobazie, puoi eliminare l'eredità. Per il punto 2. Puoi seguire la strada java e insistere su un'interfaccia per implementare questa funzione.

I vantaggi della rimozione dell'eredità sono

  1. prevenire lunghe gerarchie e i relativi problemi. Il tuo meccanismo di condivisione del codice dovrebbe essere abbastanza buono per quello.
  2. Evita i cambiamenti nelle classi genitore dall'interruzione delle classi figlio.

Il compromesso è in gran parte tra "Non ripetere te stesso" e la flessibilità da un lato e l'eliminazione dei problemi dall'altro. Personalmente odierei vedere l'eredità passare dal C ++ semplicemente perché alcuni altri sviluppatori potrebbero non essere abbastanza intelligenti da prevedere i problemi.


1

Pensi che sia giusto non consentire l'eredità o i Mixin? (dato che l'utente avrebbe almeno interfacce)

Puoi fare un sacco di lavoro molto utile senza ereditarietà dell'implementazione o mixin, ma mi chiedo se dovresti avere un qualche tipo di eredità dell'interfaccia, cioè una dichiarazione che dice che se un oggetto implementa l'interfaccia A allora deve anche interfacciare B (cioè, A è una specializzazione di B e quindi esiste una relazione di tipo). D'altra parte, l'oggetto risultante deve solo registrare che implementa entrambe le interfacce, quindi non c'è troppa complessità lì. Tutto perfettamente fattibile.

La mancanza di ereditarietà dell'implementazione ha però un chiaro svantaggio: non sarai in grado di costruire vtables indicizzati numericamente per le tue classi e quindi dovrai fare ricerche di hash per ogni chiamata di metodo (o trovare un modo intelligente per evitarle). Questo potrebbe essere doloroso se instradi anche valori fondamentali (ad es. Numeri) attraverso questo meccanismo. Anche un'implementazione di hash molto buona può essere costosa quando la colpisci più volte in ogni ciclo interno!

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.