Search code examples
haskellapplicativefree-monadalternative-functoroptparse-applicative

Generate optparse-applicative parser from free alternative functor


Consider the following type signatures:

data Foo x = Foo {
    name :: String
  , reader :: String -> x
}

instance Functor Foo where
  fmap f (Foo n r) = Foo n $ f . r

Now I show a natural transformation from Foo to optparse-applicative's Parser type:

import qualified Options.Applicative as CL

mkParser :: Foo a -> CL.Parser a
mkParser (Foo n _) = CL.option CL.disabled ( CL.long n )

(Okay, it's a bit useless, but it'll serve for discussion).

Now I take Bar to be the free alternative functor over Foo:

type Bar a = Alt Foo a

Given this is a free functor, I should be able to lift mkParser into a natural transformation from Bar to Parser:

foo :: String -> (String -> x) -> Bar x
foo n r = liftAlt $ Foo n r

myFoo :: Bar [String]
myFoo = many $ foo "Hello" (\_ -> "Hello")

clFoo :: CL.Parser [String]
clFoo = runAlt mkParser $ myFoo

And indeed, this works and gives me a Parser back. However, it's a pretty useless one, because trying to do much with it results in an infinite loop. For example, if I try to describe it:

CL.cmdDesc clFoo
> Chunk {unChunk = 

And hangs until interrupted.

The reason for this seems to be that optparse-applicative cheats in its definitions of many and some: it uses monadic parsing under the covers.

Am I doing something wrong here? I don't see how, given this, it's possible to construct a parser in this way. Any ideas?


Solution

  • As pointed in comments, you have to handle many explicitly. Approach copied from Earley:

    #!/usr/bin/env stack
    -- stack --resolver=lts-5.3 runghc --package optparse-applicative
    {-# LANGUAGE RankNTypes #-}
    {-# LANGUAGE GADTs #-}
    {-# LANGUAGE ScopedTypeVariables #-}
    
    import Control.Applicative
    import qualified Options.Applicative as CL
    import qualified Options.Applicative.Help.Core as CL
    
    data Alt f a where
      Pure   :: a                             -> Alt f a
      Ap     :: f a       -> Alt f (a -> b)   -> Alt f b
      Alt    :: [Alt f a] -> Alt f (a -> b)   -> Alt f b
      Many   :: Alt f a   -> Alt f ([a] -> b) -> Alt f b
    
    instance Functor (Alt f) where
      fmap f (Pure x)   = Pure $ f x
      fmap f (Ap x g)   = Ap x $ fmap (f .) g
      fmap f (Alt x g)  = Alt x $ fmap (f .) g
      fmap f (Many x g) = Many x $ fmap (f .) g
    
    instance Applicative (Alt f) where
      pure = Pure
    
      Pure f   <*> y = fmap f y
      Ap x f   <*> y = Ap x $ flip <$> f <*> y
      Alt xs f <*> y = Alt xs $ flip <$> f <*> y
      Many x f <*> y = Many x $ flip <$> f <*> y
    
    instance Alternative (Alt f) where
      empty = Alt [] (pure id)
      a <|> b = Alt [a, b] (pure id)
      many x  = Many x (pure id)
    
    -- | Given a natural transformation from @f@ to @g@, this gives a canonical monoidal natural transformation from @'Alt' f@ to @g@.
    runAlt :: forall f g a. Alternative g => (forall x. f x -> g x) -> Alt f a -> g a
    runAlt u = go where
        go :: forall b. Alt f b -> g b
        go (Pure x)    = pure x
        go (Ap x f)    = flip id <$> u x                           <*> go f
        go (Alt xs f)  = flip id <$> foldr (<|>) empty (map go xs) <*> go f
        go (Many x f)  = flip id <$> many (go x)                   <*> go f
    
    -- | A version of 'lift' that can be used with just a 'Functor' for @f@.
    liftAlt :: (Functor f) => f a -> Alt f a
    liftAlt x = Ap x (Pure id)
    
    mkParser :: Foo a -> CL.Parser a
    mkParser (Foo n r) = CL.option (CL.eitherReader $ Right . r) ( CL.long n CL.<> CL.help n )
    
    data Foo x = Foo {
        name :: String
      , reader :: String -> x
    }
    
    instance Functor Foo where
      fmap f (Foo n r) = Foo n $ f . r
    
    type Bar a = Alt Foo a
    
    foo :: String -> (String -> x) -> Bar x
    foo n r = liftAlt $ Foo n r
    
    myFoo :: Bar [String]
    myFoo = many $ foo "Hello" (\_ -> "Hello")
    
    clFoo :: CL.Parser [String]
    clFoo = runAlt mkParser $ myFoo
    
    main :: IO ()
    main = do
      print $ CL.cmdDesc clFoo
      print $ CL.cmdDesc $ mkParser (Foo "Hello" $ \_ -> "Hello")