Personally, my approach would be to change the association list so that the values, rather than being pairs of type `(Maybe (Parser String), String -> LispVal)`, are instead just values of type `Parser LispVal`, something like this:
hashLiteralInfo =
[ ('f', pure $ Bool False)
, ('t', pure $ Bool True)
, ('b', fmap (Number . toInteger . fromBase 2) (many $ oneOf "01"))
-- ... and so on
]
The Boolean literal cases are then just parsers which consume no input and always return the same LispVal, instead of being separate, special cases that need to be handled by awkwardly passing a dummy string to a function which ignores its argument.
In particular, with hashLiteralInfo setup as [(Char, Parser LispVal)], the parseHashLiteral function became as simple as:
parseHashLiteral :: Parser LispVal
parseHashLiteral = do
char '#'
c <- oneOf $ map fst hashLiteralInfo
case lookup c hashLiteralInfo of
Just x -> x
Nothing -> fail "Internal parse error: unregistered literal info"