8c6794b6.github.io

Extracting argument names

Can we extract argument names from haskell function?

What extracting the argument names means, when we had a function foo:

| foo :: Int -> Int -> Int -> String
| foo apple banana cherry = show (apple*60 + banana*30 + cherry*10)

We want to get the bounded variables: 'apple', 'banana', and 'cherry'. It is possible to write this argument extracter with template haskell, generics, and haskell source code representation.

We want to call the extracter function funArgs by writing something like below, in caller module:

| fooargs :: [String]
| fooargs = $(funArgs 'foo)

Loading the module in ghci, and viewing the result:

| ghci> print fooargs
| ["apple","banana","cherry"]

We see variable names: apple, banana, and cherry.

> module FunArgs (funArgs) where
>
> import Control.Monad (when)
> import Data.Generics (everything, mkQ)
> import Language.Haskell.TH hiding (Match)
> import System.Directory (doesFileExist)

Using haskell-src-exts package. Alternatively, one can use haskell-src package, when working with haskell98 source codes only. Importing with hiding Name and listE, since it conflicts with Language.Haskell.TH.

> import Language.Haskell.Exts hiding (Name, listE)

The worker function will take a Name of target function to extract variable names, and returns TH expression.

> funArgs :: Name -> ExpQ

Firstly, accessing the source code of caller. In current implementation, funArgs cannot extract a function defined in ghci interactively. In this case, merely calling error.

> funArgs fname = do
>   path <- loc_filename `fmap` location
>   exists <- runIO $ doesFileExist path
>   when (not exists) $ error $ "Cannot access: " ++ path
>   result <- runIO $ parseFileWithMode defaultParseMode path
>   case result of
>     ParseFailed l e -> error $ "Failed to parse: " ++ show l ++ ", " ++ e
>     ParseOk mdl     -> extractArgs mdl fname

When we can acces the source code, parsing the source code to get haskell representation. Parsing may fail, and in that case, showing error message with error again. There should be nicer way to handle these error cases, though we just print out the messages, and moving on for now.

Guts of argument extracter is shown below. Applying pattern match and collecting matches with everything and mkQ. When we find the target function, extracting the contents of Ident.

> extractArgs :: Module -> Name -> ExpQ
> extractArgs mdl fname = do
>    let args = everything (++) ([] `mkQ` arg) mdl
>        arg m = case m of
>          Match _ (Ident idnt) ps _ _ _
>            | idnt == name' -> map unPVar ps
>            | otherwise     -> []
>          _ -> []
>        name' = nameBase fname
>        unPVar pv = case pv of
>          PVar n -> case n of Ident i -> i; _ -> ""
>          _      -> ""
>    listE (map (litE . stringL) args)

By the way, this argument extracter was heavily inspired by language-haskell-extract.

TAGGED: haskell, templatehaskell