Perché evitare la sottotitolazione?

Ho visto molte persone della comunità di Scala consigliare di evitare la sottotitoli “come una piaga”. Quali sono le varie ragioni contro l’uso di sottotipizzazione? Quali sono le alternative?

I tipi determinano la granularità della composizione, cioè dell’estensibilità.

Ad esempio, un’interfaccia, ad esempio Comparable, che combina (quindi confonde) l’uguaglianza e gli operatori relazionali. Pertanto è imansible comporre solo una delle uguaglianza o interfaccia relazionale.

In generale, il principio di sostituzione dell’ereditarietà è indecidibile. Il paradosso di Russell implica che qualsiasi insieme che è estensibile (cioè non enumera il tipo di ogni ansible membro o sottotipo), può includere se stesso, cioè è un sottotipo di se stesso. Ma per identificare (decidere) cosa sia un sottotipo e non se stesso, gli invarianti di se stessi devono essere completamente enumerati, quindi non è più estendibile. Questo è il paradosso che l’estensibilità sottotipo rende l’eredità indecidibile. Questo paradosso deve esistere, altrimenti la conoscenza sarebbe statica e quindi la formazione della conoscenza non esisterebbe .

La composizione della funzione è la sostituzione surettiva della sottotipizzazione, perché l’input di una funzione può essere sostituito per il suo output, cioè qualsiasi dove il tipo di output è previsto, il tipo di input può essere sostituito, avvolgendolo nella chiamata di funzione. Ma la composizione non rende il contratto biettivo di sottotitolare – l’accesso all’interfaccia dell’output di una funzione, non accede all’istanza di input della funzione.

Quindi la composizione non deve mantenere invarianti futuri (cioè illimitati) e quindi può essere sia estensibile che decidibile. La sottotipizzazione può essere MOLTO più potente laddove sia provabile decidibile, perché mantiene questo contratto biettivo, ad esempio una funzione che ordina una lista immutabile del supertipo, può operare sulla lista immutabile del sottotipo.

Quindi la conclusione è quella di enumerare tutti gli invarianti di ogni tipo (cioè delle sue interfacce), rendere questi tipi ortogonali (massimizzare la granularità della composizione), e quindi usare la composizione della funzione per realizzare un’estensione dove quegli invarianti non sarebbero ortogonali. Quindi un sottotipo è appropriato solo dove dimostra in modo dimostrabile gli invarianti dell’interfaccia del supertipo e le interfacce aggiuntive del sottotipo sono provvisamente ortogonali agli invarianti dell’interfaccia del supertipo. Quindi gli invarianti delle interfacce dovrebbero essere ortogonali.

La teoria delle categorie fornisce regole per il modello degli invarianti di ciascun sottotipo, cioè di Functor, Applicative e Monad, che preservano la composizione della funzione sui tipi sollevati , ovvero si veda l’esempio sopra menzionato della potenza della sottotipizzazione delle liste.

Una ragione è che equals () è molto difficile da ottenere quando è implicata la sub-typing. Vedi Come scrivere un metodo di uguaglianza in Java . Specificamente “Trappola n. 4: non riuscire a definire equivale come una relazione di equivalenza”. In sostanza: per ottenere l’uguaglianza proprio sotto la sub-tipizzazione, è necessario un doppio invio.

Penso che il contesto generale sia per la lanaguage essere il più “puro” ansible (cioè usare il più ansible le pure funzioni ), e viene dal confronto con Haskell.
Da ” Ruminations of a Programmer ”

Scala, essendo un linguaggio OO-FP ibrido, deve prendersi cura di problemi come il sottotipo (che Haskell non ha).

Come menzionato in questa risposta PSE :

nessun modo per limitare un sottotipo in modo che non possa fare più del tipo da cui eredita.
Ad esempio, se la class base è immutabile e definisce un metodo puro foo(...) , le classi derivate non devono essere modificabili o sovrascrivere foo() con una funzione che non è pura

Ma la vera raccomandazione sarebbe quella di utilizzare la soluzione migliore adattata al programma che stai attualmente sviluppando.

Concentrandosi su sottotipizzazione, ignorando i problemi relativi a classi, ereditarietà, OOP, ecc. Abbiamo l’idea che la sottotipizzazione rappresenta una relazione isa tra i tipi. Ad esempio, i tipi A e B hanno diverse operazioni, ma se A isa B possiamo usare una qualsiasi delle operazioni di B su un A.

OTOH, usando un’altra relazione tradizionale, se C ha una B allora possiamo riutilizzare qualsiasi operazione di B su una C. Di solito le lingue ti permettono di scriverne una con una syntax più bella, a.opOnB invece di a.super.opOnB come sarebbe nel caso di composizione, cbopOnB

Il problema è che in molti casi c’è più di un modo per mettere in relazione due tipi. Ad esempio Real può essere incorporato in Complex assumendo 0 nella parte immaginaria, ma Complex può essere incorporato in Real ignorando la parte immaginaria, in modo che entrambi possano essere visti come sottotipi dell’altro e le sottotipizzazioni costringano una relazione a essere vista come preferita. Inoltre, ci sono più possibili relazioni (es. Visualizzare Complesso come Reale usando la componente theta della rappresentazione polare).

Nella terminologia formale di solito diciamo morfismo a tali relazioni tra tipi e ci sono tipi speciali di morfismi per relazioni con proprietà diverse (es. Isomorfismo, omomorfismo).

In una lingua con sottotitoli di solito c’è molto più zucchero sulle relazioni isa e date molte possibili incursioni tendiamo a vedere attriti inutili ogni volta che usiamo la relazione non preferita. Se portiamo ereditarietà, classi e OOP al mix, il problema diventa molto più visibile e disordinato.

La mia risposta non risponde perché viene evitata ma cerca di dare un altro suggerimento sul perché possa essere evitata.

Utilizzando “classi di tipi” è ansible aggiungere un’astrazione su tipi / classi esistenti senza modificarli. L’ereditarietà è usata per esprimere che alcune classi sono specializzazioni di una class più astratta. Ma con le classi di tipi puoi prendere qualsiasi class esistente ed esprimere che tutti condividono una proprietà comune, ad esempio sono Comparable . E finché non ti preoccupi che siano Comparable non te ne accorgi nemmeno. Le classi non ereditano alcun metodo da un tipo Comparable astratto purché non le utilizzi. È un po ‘come programmare in linguaggi dinamici.

Ulteriori letture:

http://blog.tmorris.net/the-power-of-type-classs-with-scala-implicit-defs/

http://debasishg.blogspot.com/2010/07/refactoring-into-scala-type-classs.html

Non conosco Scala, ma penso che il mantra “preferisca la composizione sull’ereditarietà” si applica a Scala esattamente come fa per ogni altro linguaggio di programmazione OO (e la sottotipizzazione è spesso usata con lo stesso significato di “eredità”). Qui

Preferisci la composizione all’ereditarietà?

troverai qualche informazione in più.

Penso che molti programmatori di Scala siano ex programmatori Java. Sono abituati a pensare in termini di sottotitoli Object Oriented e dovrebbero essere in grado di trovare facilmente una soluzione simile a OO per la maggior parte dei problemi. Ma il Functional Programing è un nuovo paradigma da scoprire, quindi le persone chiedono un diverso tipo di soluzioni.

Questa è la migliore carta che ho trovato sull’argomento. Una citazione motivante dal giornale –

Sosteniamo che mentre alcuni degli aspetti più semplici dei linguaggi orientati agli oggetti sono compatibili con ML, l’aggiunta di un sistema di oggetti basato su classi completo a ML conduce a un sistema di tipi eccessivamente complesso e relativamente poco guadagno espressivo