Search code examples
haskellparsecfunctional-dependencies

Using functional dependency to eliminate type parameter


I am trying to implement a Parsec Stream wrapper that will remember the last uncons'd token in order to provide some look-behind capability. I want the wrapper to work with any Stream instance. Here's what I have so far:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
module MStream where

import Text.Parsec
import Control.Monad ( liftM )

data MStream s t = MStream (Maybe t) s

instance Stream s m t => Stream (MStream s t) m t where
  uncons (MStream _ s) = fmap (\(t, s') -> (t, MStream (Just t) s')) `liftM` uncons s

getPrevToken :: Stream s m t => ParsecT (MStream s t) u m (Maybe t)
getPrevToken = (\(MStream t _) -> t) `liftM` getInput

mstream :: s -> MStream s t
mstream = MStream Nothing

This works, but I don't like having to carry the t parameter in the MStream type constructor. Surely it should be sufficient to require only the s parameter, since t can be derived from s as long as there is a witness for Stream s m t. I've tried using type families and GADTs, but I keep running into obscure errors about ambiguous type variables and unsatisfied functional dependencies.

Is there a way to remove t from the MStream type constructor so I don't have to write:

sillyParser :: Stream s m Char => ParsecT (MStream s Char) u m String
sillyParser = do
  t <- getPrevToken
  maybe (string "first") (\c -> string $ "last" ++ [c]) t

Solution

  • So, I solved this by moving the goalposts (somewhat):

    {-# LANGUAGE FlexibleContexts, FlexibleInstances,
                 MultiParamTypeClasses, FunctionalDependencies #-}
    
    module MStream where
    
    import Text.Parsec
    import Control.Monad ( liftM )
    
    class Stream s m t => MStream s m t | s -> t where
      getPrevToken :: ParsecT s u m (Maybe t)
    
    data MStreamImpl s t = MStreamImpl (Maybe t) s
    
    instance Stream s m t => MStream (MStreamImpl s t) m t where
      getPrevToken = (\(MStreamImpl t _) -> t) `liftM` getInput
    
    instance Stream s m t => Stream (MStreamImpl s t) m t where
      uncons (MStreamImpl _ s) = fmap (\(t, s') -> (t, MStreamImpl (Just t) s')) `liftM` uncons s
    
    mstream :: s -> MStreamImpl s t
    mstream = MStreamImpl Nothing
    
    sillyParser :: MStream s m Char => ParsecT s u m String
    sillyParser = do
      t <- getPrevToken
      maybe (string "first") (\c -> string $ "last" ++ [c]) t
    

    Instead of trying to remove the type parameter from MStream, I turned MStream into a typeclass, with a canonical instance MStreamImpl. So now, the sillyParser type signature can be written in a more compact manner simply by replacing the Stream context with an MStream context.