Search code examples
haskellmonad-transformersstate-monad

extracting names from a list of (name,handler) pairs


Here is a simple example using haskeline with the StateT transformer to create a stateful input command loop:

{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.State.Strict
import Control.Monad.Trans (lift)
import System.Console.Haskeline

main =  runStateT (runInputT defaultSettings loop) ""

check ma b fb = maybe b fb ma

commands :: (MonadState String m, MonadIO m) => [(String, [String] -> InputT m ())]
commands = [ ("set", performSet), ("get", performGet) ]

performSet args = lift $ put (head args)
performGet _ = do v <- lift get; outputStrLn $ "v = " ++ show v

loop :: (MonadException m, MonadState String m) => InputT m ()
loop = do
   minput <- getInputLine "% "
   check minput (return ()) $ \inp -> do
     let args = words inp
     case args of
       [] -> loop
       (arg0:argv) -> do
         case lookup arg0 commands of
           Nothing      -> do outputStrLn "huh?"; loop
           Just handler -> do handler argv; loop

The list commands contains all of the recognized commands - pairs of (name, handler). I want to get the list of names using an expression like:

commandNames = map fst commands

but the type checker complains with "No instance for (MonadState String m0) arising from a use of ‘commands’ - The type variable ‘m0’ is ambiguous ..."

What do I need to do to satisfy the type checker?


Solution

  • commands is polymorphic, it has a type variable, m, but commandNames :: [String] doesn't have any type variables. This means that (barring some built-in defaults) type inference won't be able to infer the type variable for commands. There are two things you can do. You can provide a type for commands yourself

    commandNames :: [String]
    commandNames = map fst (commands :: [(String, [String] -> InputT (StateT String IO) ())])
    

    Or you can change your code so that the value with more type variables is defined in terms of the one with fewer.

    commandNames :: [String]
    commandNames = ["set", "get"]
    
    commands :: (MonadState String m, MonadIO m) => [(String, [String] -> InputT m ())]
    commands = zip commandNames [performSet, performGet]