Me advierten que el Monoid que estoy creando es una instancia huérfana. ¿Hay una mejor manera de escribir esta funcionalidad en?

7
type PromptSegment = IO (Maybe String)

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

Esto se comporta exactamente como quiero para mis propósitos.

Por ejemplo:

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"

Sin embargo, estoy bastante consciente de que quizás, para un Monoid diferente a = > ¿Monoide (IO a) instancia que no sea un PromptSegment, tal vez no quisiera que Mappend se comporte de la misma manera? Siento que hay una mejor manera de hacerlo que crear la instancia de monoides huérfanos de arriba.

Diseño de API actual

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

Este es, por supuesto, un trabajo en progreso, pero el objetivo del programa es crear un DSL para definir los temas del mensaje Cli (comenzando con ZSH). Este último ejemplo de código es principalmente para servir al propósito de proporcionar contexto a la pregunta de por qué querría hacer IO (quizás String) un Monoide. Estoy abierto a las duras críticas, especialmente porque este es mi primer intento de escribir un programa "real" en Haskell.

    
pregunta Josiah 07.11.2014 - 02:15

1 respuesta

6

Hay una razón detrás de advertencia de nuevo en instancias huérfanas . En particular: si comenzamos a usar instancias huérfanas, los módulos pueden volverse mutuamente incompatibles: ¿Qué sucede si dos módulos definen dos instancias diferentes Monoid (IO a) ? No hay una buena manera de saber cuál debería ser la preferida.

Además, el uso de instancias huérfanas a menudo conduce a un mal diseño del programa. Si agrega una instancia huérfana a un tipo de datos, está agregando alguna funcionalidad al tipo de datos. Y tarde o temprano, también deberá modificar la funcionalidad, que no es posible (por diseño) con las clases de tipo Haskell. En este punto, ya sea que tenga que refactorizar su programa o comenzar a doblar las reglas, puede desordenar su diseño. (Esto es diferente del mundo OO donde las subclases tienen la intención de modificar algunas de las funciones de sus padres).

Por lo tanto, sugiero encarecidamente que cree un nuevo tipo de datos para su caso de uso. Pagará un pequeño precio de una sola vez por crear alias para las funciones que usará en su proyecto (probablemente en un módulo dedicado), como

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

o más generalmente definen la instancia MonadIO MyDSL y luego

getCurrentDirectory :: MonadIO FilePath
getCurrentDirectory = liftIO D.getCurrentDirectory

Pero entonces su implementación será completamente independiente y, a medida que su proyecto evolucione, nada lo limitará a hacer que los elementos internos de MyDSL sean más complejos o agregar funcionalidad. Además, será seguro utilizarlo como una biblioteca en otros proyectos, sin el riesgo de obtener instancias conflictivas.

Dicha separación es aún más importante cuando creas un DSL, porque en este caso quieres separar el idioma de su implementación interna. El uso de un tipo de datos existente para un DSL es muy probable que cause problemas, ya sea al implementarlo o debido a un uso inadecuado por parte de sus usuarios.

    
respondido por el Petr Pudlák 07.11.2014 - 13:30

Lea otras preguntas en las etiquetas