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?
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).