Search code examples
haskelltypeclasspolyvariadic

Haskell function with different number of argument


I'm trying to create a Haskell function with a class to get this function to work with different numbers of arguments.

{-# Language FlexibleInstances #-}

class Titles a where
  titleTeX ::  String -> a

instance Titles String where
  titleTeX str = titleWithFrame 1 "%" "%" "%" [str]

instance  Titles (String -> String) where
  titleTeX str = (\s -> titleWithFrame 1 "%" "%" "%" (s:[str]))

titleWithFrame::Int -> String -> String -> String -> [String] -> String
titleWithFrame nb beg end com lstr =
  cadr++cont++cadr
    where
          cadr = concat $ replicate nb (beg++rempl++end++"\n")
          cont = concatMap (\s -> beg++" "++s++" "++end++"\n") lstr
          rempl = take long $ concat $ replicate long com
          long = (maximum $ map length lstr) + 2

When I try this function with ghci, I have the following results:

ghci> putStr $ titleTeX "Line 1"
%%%%%%%%%%
% Line 1 %
%%%%%%%%%%
ghci> putStr $ titleTeX "Line 1" "Line 2"
%%%%%%%%%%
% Line 1 %
% Line 2 %
%%%%%%%%%%
ghci> putStr $ titleTeX "Line 1" "Line 2" "Line 3"

<interactive>:4:10: error:
    • No instance for (Main.Titles ([Char] -> [Char] -> String))
        arising from a use of ‘titleTeX’
        (maybe you haven't applied a function to enough arguments?)
    • In the second argument of ‘($)’, namely
        ‘titleTeX "Line 1" "Line 2" "Line 3"’
      In the expression: putStr $ titleTeX "Line 1" "Line 2" "Line 3"
      In an equation for ‘it’:
          it = putStr $ titleTeX "Line 1" "Line 2" "Line 3"

I don't understand where my error is and why my polyvariadic function doesn't work with more than 2 arguments.

Do you know where my error comes from? and How to make my function work with an arbitrary number of arguments?


Solution

  • The error occurs because you have exactly two instances of Titles in your program:

    instance Titles String
    instance Titles (String -> String)
    

    These let you call titleTeX with one and two arguments respectively, but three arguments would require

    instance Titles (String -> String -> String)
    

    which doesn't exist. Or as ghc puts it:

    • No instance for (Main.Titles ([Char] -> [Char] -> String))
        arising from a use of ‘titleTeX’
    

    ([Char] is the same as String.)

    It's as if you'd defined a function

    foo :: [Int] -> Int
    foo [x] = ...
    foo [x, y] = ...
    

    but foo [x, y, z] is an error.

    To make this work for any number of arguments, we need to use recursion. As with list functions (where you'd typically have a base case foo [] = ... and a recursive case foo (x : xs) = ... that calls foo xs somewhere), we need to define a Titles instance in terms of other instances:

    instance Titles String
    instance (Titles a) => Titles (String -> a)
    

    The tricky bit is that I don't see a way to implement a titleTeX that fits the above declarations.

    I had to make other changes to your code to make it work:

    {-# Language FlexibleInstances #-}
    
    titleTeX :: (Titles a) => String -> a
    titleTeX str = titleTeXAccum [str]
    

    titleTeX isn't a method anymore. It's just a convenience front-end for the actual titleTeXAccum method.

    In principle we could have omitted the String parameter and defined titleTeX :: (Titles a) => a as titleTeX = titleTexAccum [], but then titleTex :: String would crash at runtime (because we end up calling maximum on an empty list).

    class Titles a where
      titleTeXAccum :: [String] -> a
    

    Our method now takes a list of strings that it (somehow) turns into a value of type a.

    instance Titles String where
      titleTeXAccum acc = titleWithFrame 1 "%" "%" "%" (reverse acc)
    

    The implementation for String is easy: We just call titleWithFrame. We also pass reverse acc because the order of elements in the accumulator is backwards (see below).

    instance (Titles a) => Titles (String -> a) where
      titleTeXAccum acc str = titleTeXAccum (str : acc)
    

    This is the crucial part: The general titleTeXAccum method forwards to another titleTeXAccum method (of a different type / different Titles instance). It adds str to the accumulator. We could have written acc ++ [str] to add the new element at the end, but that's inefficient: Calling titleTeXAccum with N elements would take O(N^2) time (due to repeated list traversals in ++). Using : and only calling reverse once at the end reduces this to O(N).