functionhaskelltypestransparencyreferential

Haskell: type variables and generic types


When I mention in the type signature of the function isQuestion the types explicitly, GHCi compiles it perfectly:

isQuestion :: [Char] -> Maybe Bool
isQuestion [] = Nothing
isQuestion xs = Just (last xs == '?')

However, when I turn to 'generic' code, it doesn't work:

isQuestion :: [a] -> Maybe b
isQuestion [] = Nothing
isQuestion xs = Just (last xs == '?')

since I get the below error:

<interactive>:138:17: error:
    * Couldn't match type `b' with `Bool'
      `b' is a rigid type variable bound by
        the type signature for:
          isQuestion :: forall a b. [a] -> Maybe b
        at <interactive>:136:1-28
      Expected type: Maybe b
        Actual type: Maybe Bool
    * In the expression: Just (last xs == '?')
      In an equation for `isQuestion':
          isQuestion xs = Just (last xs == '?')
    * Relevant bindings include
        isQuestion :: [a] -> Maybe b (bound at <interactive>:137:1)

Solution

  • First observation is that

    last xs == something
    

    can only work if there is a definition of (==) in scope for the elements of xs. But as the compiler knows nothing about a, there is no such thing. You must narrow a to be the subset of types with equality:

    isQuestion :: Eq a => [a] -> Maybe b
    

    The second observation is that something is a Char in your code ('?'), so this method can only ever work when a ≡ Char. Instead, you could add that something as a parameter:

    isQuestion :: Eq a => a -> [a] -> Maybe b
    

    Finally, as has been pointed out, you have a concrete return type, i.e. Maybe Bool, as

    (==) :: a -> a -> Bool
    

    So your function's signature could be

    isQuestion :: Eq a => a -> [a] -> Maybe Bool
    

    Edited this paragraph for clarity Note that, depending on your business logic, you might not want to make a special case of the empty string. If the only thing you care about is whether a string ends with a question mark, then Nothing and Just false would virtually mean the same thing. In that case, your function becomes a "yes or no" question and you can drop the Maybe:

    isQuestion :: Eq a => a -> [a] -> Bool
    isQuestion x = isSuffixOf [x]
    

    or simply

    isQuestion :: String -> Bool
    isQuestion = isSuffixOf "?"