Istanze orfane a Haskell


86

Quando compilo la mia applicazione Haskell con l' -Wallopzione, GHC si lamenta delle istanze orfane, ad esempio:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

La classe del tipo ToSElemnon è mia, è definita da HStringTemplate .

Ora so come risolvere questo problema (sposta la dichiarazione dell'istanza nel modulo in cui viene dichiarato il risultato) e so perché GHC preferirebbe evitare le istanze orfane , ma continuo a credere che la mia strada sia migliore. Non mi interessa se il compilatore è disturbato, piuttosto che io.

Il motivo per cui desidero dichiarare le mie ToSElemistanze nel modulo Publisher è perché è il modulo Publisher che dipende da HStringTemplate, non dagli altri moduli. Sto cercando di mantenere una separazione delle preoccupazioni ed evitare che ogni modulo dipenda da HStringTemplate.

Ho pensato che uno dei vantaggi delle classi di tipo Haskell, rispetto ad esempio alle interfacce di Java, è che sono aperte piuttosto che chiuse e quindi le istanze non devono essere dichiarate nello stesso posto del tipo di dati. Il consiglio di GHC sembra essere quello di ignorarlo.

Quindi, quello che cerco è una conferma che il mio pensiero è corretto e che sarei giustificato ignorando / sopprimendo questo avvertimento, o un argomento più convincente contro il fare le cose a modo mio.


La discussione nelle risposte e nei commenti illustra che c'è una grande differenza tra la definizione di istanze orfane in un eseguibile , come stai facendo, e in una libreria esposta ad altri. Questa domanda estremamente popolare illustra quanto possano creare confusione le istanze orfane per gli utenti finali di una libreria che le definisce.
Christian Conkle

Risposte:


94

Capisco perché vuoi farlo, ma sfortunatamente può essere solo un'illusione che le lezioni Haskell sembrino "aperte" nel modo in cui dici. Molte persone pensano che la possibilità di farlo sia un bug nelle specifiche Haskell, per ragioni che spiegherò di seguito. Ad ogni modo, se davvero non è appropriato per l'istanza devi essere dichiarato o nel modulo in cui viene dichiarata la classe o nel modulo in cui viene dichiarato il tipo, questo è probabilmente un segno che dovresti usare un newtypeo qualche altro wrapper intorno al tuo tipo.

I motivi per cui le istanze orfane devono essere evitate sono molto più profondi della comodità del compilatore. Questo argomento è piuttosto controverso, come puoi vedere da altre risposte. Per bilanciare la discussione, spiegherò il punto di vista secondo cui non si dovrebbero mai, mai scrivere istanze orfane, che penso sia l'opinione della maggioranza tra gli Haskeller esperti. La mia opinione è da qualche parte nel mezzo, che spiegherò alla fine.

Il problema deriva dal fatto che quando esiste più di una dichiarazione di istanza per la stessa classe e tipo, non esiste alcun meccanismo nello standard Haskell per specificare quale utilizzare. Piuttosto, il programma viene rifiutato dal compilatore.

L'effetto più semplice è che potresti avere un programma perfettamente funzionante che interromperà improvvisamente la compilazione a causa di un cambiamento che qualcun altro fa in una dipendenza lontana del tuo modulo.

Ancora peggio, è possibile che un programma funzionante inizi a bloccarsi in fase di esecuzione a causa di un cambiamento lontano. Potresti usare un metodo che presumi provenga da una certa dichiarazione di istanza e potrebbe essere silenziosamente sostituito da un'istanza diversa che è abbastanza diversa da far sì che il tuo programma inizi a bloccarsi in modo inspiegabile.

Le persone che vogliono garanzie che questi problemi non si verifichino mai loro devono seguire la regola che se qualcuno, ovunque, ha mai dichiarato un'istanza di una certa classe per un certo tipo, nessun'altra istanza deve mai essere dichiarata di nuovo in nessun programma scritto da chiunque. Ovviamente, c'è la soluzione alternativa di utilizzare a newtypeper dichiarare una nuova istanza, ma questo è sempre almeno un inconveniente minore, e talvolta anche grave. Quindi, in questo senso, coloro che scrivono intenzionalmente istanze orfane sono piuttosto scortesi.

Allora cosa si dovrebbe fare per risolvere questo problema? Il campo anti-istanze orfane afferma che l'avviso GHC è un bug, deve essere un errore che rifiuta qualsiasi tentativo di dichiarare un'istanza orfana. Nel frattempo, dobbiamo esercitare l'autodisciplina ed evitarli a tutti i costi.

Come hai visto, ci sono quelli che non sono così preoccupati per questi potenziali problemi. In realtà incoraggiano l'uso di istanze orfane come strumento per la separazione delle preoccupazioni, come suggerisci, e affermano che si dovrebbe solo assicurarsi caso per caso che non ci siano problemi. Sono stato infastidito abbastanza volte dalle istanze orfane di altre persone per convincermi che questo atteggiamento è troppo sprezzante.

Penso che la soluzione giusta sarebbe aggiungere un'estensione al meccanismo di importazione di Haskell che controllerebbe l'importazione delle istanze. Ciò non risolverebbe completamente i problemi, ma fornirebbe un aiuto per proteggere i nostri programmi dai danni delle istanze orfane che già esistono nel mondo. E poi, con il tempo, potrei convincermi che in alcuni casi limitati, forse un'istanza orfana potrebbe non essere così grave. (E proprio quella tentazione è la ragione per cui alcuni nel campo anti-orfani si oppongono alla mia proposta.)

La mia conclusione da tutto ciò è che, almeno per il momento, ti consiglio caldamente di evitare di dichiarare casi orfani, di essere rispettoso degli altri se non per altro. Usa un file newtype.


4
In particolare, questo è sempre più un problema con la crescita delle biblioteche. Con oltre 2200 librerie su Haskell e decine di migliaia di singoli moduli, il rischio di raccogliere istanze aumenta notevolmente.
Don Stewart,

16
Oggetto: "Penso che la soluzione giusta sarebbe aggiungere un'estensione al meccanismo di importazione di Haskell che controllerebbe l'importazione di istanze" Nel caso in cui questa idea interessi qualcuno, potrebbe valere la pena guardare il linguaggio Scala per un esempio; ha caratteristiche molto simili a questa per controllare l'ambito degli "impliciti", che possono essere usati in modo molto simile alle istanze della classe di tipi.
Matt

5
Il mio software è un'applicazione piuttosto che una libreria, quindi la possibilità di causare problemi ad altri sviluppatori è praticamente nulla. Potresti considerare il modulo Publisher l'applicazione e il resto dei moduli come una libreria, ma se dovessi distribuire la libreria sarebbe senza il Publisher e, quindi, le istanze orfane. Ma se spostassi le istanze negli altri moduli, la libreria verrebbe fornita con una dipendenza non necessaria da HStringTemplate. Quindi in questo caso penso che gli orfani stiano bene, ma ascolterò il tuo consiglio se dovessi riscontrare lo stesso problema in un contesto diverso.
Dan Dyer,

1
Sembra un approccio ragionevole. L'unica cosa a cui prestare attenzione è se l'autore di un modulo che importi aggiunge questa istanza in una versione successiva. Se tale istanza è uguale alla tua, dovrai eliminare la tua dichiarazione di istanza. Se quell'istanza è diversa dalla tua, dovrai inserire un wrapper newtype attorno al tuo tipo, che potrebbe essere un refactoring significativo del tuo codice.
Yitz

@ Matt: in effetti, sorprendentemente Scala ottiene questo proprio dove Haskell no! (tranne ovviamente che Scala manca di sintassi di prima classe per i macchinari di classe di tipo, che è anche peggio ...)
Erik Kaplun

44

Vai avanti e sopprimi questo avvertimento!

Sei in buona compagnia. Conal lo fa in "TypeCompose". "chp-mtl" e "chp-transformers" lo fanno, "control-monad-exception-mtl" e "control-monad-exception-monadsfd" lo fanno, ecc.

btw probabilmente lo sai già, ma per quelli che non lo fanno e inciampano la tua domanda in una ricerca:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Modificare:

Riconosco i problemi che Yitz ha menzionato nella sua risposta come problemi reali. Tuttavia, non vedo nemmeno l'utilizzo di istanze orfane come un problema, e cerco di scegliere il "minimo di tutti i mali", che è imho usare prudentemente le istanze orfane.

Ho usato solo un punto esclamativo nella mia breve risposta perché la tua domanda mostra che sei già ben consapevole dei problemi. Altrimenti sarei stato meno entusiasta :)

Un po 'un diversivo, ma quella che credo sia la soluzione perfetta in un mondo perfetto senza compromessi:

Credo che i problemi menzionati da Yitz (non sapendo quale istanza viene scelta) potrebbero essere risolti in un sistema di programmazione "olistico" dove:

  • Non stai modificando semplici file di testo in modo primitivo, ma sei piuttosto assistito dall'ambiente (ad esempio il completamento del codice suggerisce solo cose di tipi rilevanti ecc.)
  • Il linguaggio di "livello inferiore" non ha un supporto speciale per le classi di tipo, e invece le tabelle delle funzioni vengono passate esplicitamente
  • Ma l'ambiente di programmazione "di livello superiore" mostra il codice in modo simile a come viene presentato Haskell ora (di solito non vedrai le tabelle delle funzioni passate) e seleziona le classi di tipo esplicite quando sono ovvie (per esempio tutti i casi di Functor hanno una sola scelta) e quando ci sono diversi esempi (zipping list Applicative o list-monad Applicative, First / Last / lift forse Monoid) ti permette di scegliere quale istanza usare.
  • In ogni caso, anche quando l'istanza è stata selezionata automaticamente, l'ambiente ti permette facilmente di vedere quale istanza è stata utilizzata, con un'interfaccia semplice (un collegamento ipertestuale o un'interfaccia hover o qualcosa del genere)

Di ritorno dal mondo fantastico (o, si spera, dal futuro), proprio ora: consiglio di cercare di evitare le istanze orfane continuando a usarle quando "ne hai davvero bisogno"


5
Sì, ma probabilmente ognuno di questi eventi è un errore di un certo ordine. Mi vengono in mente i cattivi casi in control-monad-exception-mtl e monads-fd per Either. Sarebbe meno invadente se ciascuno di quei moduli fosse costretto a definire i propri tipi o fornire wrapper di nuovo tipo. Quasi ogni istanza orfana è un mal di testa in attesa di accadere e, se non altro, richiederà la tua costante vigilanza per garantire che sia importato o meno come appropriato.
Edward KMETT,

2
Grazie. Penso che li userò in questa particolare situazione, ma grazie a Yitz ora ho una migliore comprensione dei problemi che possono causare.
Dan Dyer,

37

Le istanze orfane sono una seccatura, ma secondo me a volte sono necessarie. Spesso combino librerie in cui un tipo proviene da una libreria e una classe proviene da un'altra libreria. Ovviamente non ci si può aspettare che gli autori di queste librerie forniscano istanze per ogni possibile combinazione di tipi e classi. Quindi devo provvedere a loro, e quindi sono orfani.

L'idea che dovresti racchiudere il tipo in un nuovo tipo quando devi fornire un'istanza è un'idea con merito teorico, ma è semplicemente troppo noiosa in molte circostanze; è il tipo di idea avanzata da persone che non scrivono codice Haskell per vivere. :)

Quindi vai avanti e fornisci istanze orfane. Sono innocui.
Se puoi mandare in crash ghc con istanze orfane, allora questo è un bug e dovrebbe essere segnalato come tale. (Il bug che ghc aveva / ha sul non rilevare più istanze non è così difficile da risolvere.)

Ma tieni presente che in futuro qualcun altro potrebbe aggiungere l'istanza che hai già e potresti ricevere un errore (in fase di compilazione).


2
Un buon esempio è (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)quando si utilizza QuickCheck.
Erik Kaplun

17

In questo caso, penso che l'uso di istanze orfane vada bene. La regola generale per me è: puoi definire un'istanza se "possiedi" la classe di tipi o se "possiedi" il tipo di dati (o qualche suo componente - cioè, anche un'istanza per Forse MyData va bene, almeno a volte). Entro questi vincoli, dove decidi di inserire l'istanza sono affari tuoi.

C'è un'ulteriore eccezione: se non possiedi né il typeclass né il tipo di dati, ma stai producendo un binario e non una libreria, allora va bene anche questo.


5

(So ​​di essere in ritardo alla festa ma questo potrebbe essere ancora utile per gli altri)

È possibile mantenere le istanze orfane nel proprio modulo, quindi se qualcuno importa quel modulo è specificamente perché ne ha bisogno e può evitare di importarle se causa problemi.


3

In questo senso, capisco la posizione delle librerie WRT del campo di istanze anti-orfane, ma per le destinazioni eseguibili non dovrebbero andare bene le istanze orfane?


3
In termini di scortesia con gli altri, hai ragione. Ma ti stai aprendo a potenziali problemi futuri se la stessa istanza viene mai definita in futuro da qualche parte nella tua catena di dipendenze. Quindi, in questo caso, sta a te decidere se vale la pena rischiare.
Yitz

5
In quasi tutti i casi di implementazione di un'istanza orfana in un eseguibile, è per colmare una lacuna che desideri fosse già definita. Quindi, se l'istanza appare a monte, l'errore di compilazione risultante è solo un segnale utile per dirti che puoi rimuovere la tua dichiarazione dell'istanza.
Ben
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.