Mi viene detto che il Monoid che sto creando è un'istanza orfana. C'è un modo migliore per scrivere questa funzionalità?

7
type PromptSegment = IO (Maybe String)

instance Monoid a => Monoid (IO a) where
  mempty = return mempty
  mappend = liftA2 (<>)

Questo si comporta esattamente come voglio per i miei scopi.

Ad esempio:

ghci> let a = return $ Just "hello" :: IO Maybe String
ghci> let b = return $ Just " world" :: IO Maybe String
ghci> let c = return $ Nothing :: IO Maybe String
ghci> a 'mappend' b 'mappend' c
Just "hello world"

Tuttavia, sono abbastanza consapevole che forse, per un Monoid diverso a = > Monoid (IO a) istanza diversa da un PromptSegment, forse non vorrei che il mappend si comportasse allo stesso modo ?? Mi sembra che ci sia un modo migliore per farlo rispetto alla creazione dell'istanza monoid orfana sopra.

Design dell'API corrente

currentDirectory :: PromptSegment
currentDirectory = Just <$> getCurrentDirectory

main :: IO ()
main = buildMainPrompt
         [ bold . fgColor skyBlue <$> currentDirectory
         ,  (fgColor deepSkyBlue3 . underline . bold <$> gitCurrentBranch)
           <> (fgColor defaultDarkGreen . bold <$> gitRepositorySymbol "±")
           <> gitStatusSegment
         ]
         (fgColor red0 . bold <$> makePromptSegment " ➢ ")
         (fgColor slateBlue0 . bold <$> makePromptSegment " λ» ")

gitStatusSegment :: PromptSegment
gitStatusSegment =
  let unstagedSymbol = fgColor gold1 <$> gitUnstagedSymbol "✚"
      stagedSymbol   = fgColor orange <$> gitStagedSymbol "✎"
      pushSymbol     = fgColor red1 . bold <$> gitPushSymbol "↑"
  in prependSpace <$> unstagedSymbol <> stagedSymbol <> pushSymbol

Questo è, naturalmente, un work in progress, ma lo scopo del programma è creare un DSL per definire i temi del prompt dei cli (a partire da ZSH). Questo ultimo esempio di codice serve principalmente a fornire il contesto la domanda per il motivo per cui vorrei rendere IO (Maybe String) un Monoid. Sono spalancato alle aspre critiche, soprattutto perché questo è il mio primo tentativo di scrivere un programma "reale" in Haskell.

    
posta Josiah 07.11.2014 - 02:15
fonte

1 risposta

6

Esistono motivi alla base di avviso di nuovo istanze orfane . In particolare: se iniziamo a utilizzare le istanze orfane, i moduli possono diventare reciprocamente incompatibili: cosa succede se due moduli definiscono due diverse istanze Monoid (IO a) ? Non c'è un buon modo per dire quale dovrebbe essere preferito.

Inoltre, l'utilizzo di istanze orfane porta spesso a una cattiva progettazione del programma. Se aggiungi un'istanza orfana a un tipo di dati, stai aggiungendo alcune funzionalità al tipo di dati. E prima o poi dovrai anche modificare la funzionalità, che non è (di progettazione) possibile con le classi di tipi Haskell. A questo punto devi rifattorizzare il tuo programma o iniziare a piegare le regole per aggiungere confusione al tuo progetto. (Questo è diverso dal mondo OO in cui le sottoclassi hanno lo scopo di modificare alcune funzionalità dei loro genitori.)

Pertanto suggerisco caldamente di creare un nuovo tipo di dati per il tuo caso d'uso. Pagherai un piccolo prezzo una tantum per la creazione di alias per le funzioni che utilizzerai nel tuo progetto (molto probabilmente in un modulo dedicato), come

import qualified System.Directory as D
getCurrentDirectory :: MyDSL FilePath
getCurrentDirectory = --  something wraping D.getCurrentDirectory 

o più generalmente definisci l'istanza MonadIO MyDSL e poi

getCurrentDirectory :: MonadIO FilePath
getCurrentDirectory = liftIO D.getCurrentDirectory

Ma poi la tua implementazione sarà completamente indipendente e man mano che il tuo progetto si evolverà, nulla ti limiterà a rendere gli interni di MyDSL più complessi o aggiungere funzionalità. Inoltre, sarà sicuro da usare come libreria in altri progetti, senza il rischio di ottenere istanze in conflitto.

Tale separazione è ancora più importante quando si crea un DSL, perché in questo caso si desidera strongmente separare la lingua dalla sua implementazione interna. L'utilizzo di un tipo di dati esistente per un DSL è molto probabile che causi problemi, sia durante l'implementazione che a causa di un uso improprio da parte dei suoi utenti.

    
risposta data 07.11.2014 - 13:30
fonte