
I think I'm starting too see what my problem is. I think it boils down to hankering for Duck Typing and variadic functions. I fully appreciate that passing functions is a wonderful and powerful technique for catering for variation, but Haskell's type system cramps my style by insisting that I can't put a (Banana -> Cake) in the same container as an (Octopus -> Truffles -> DogsBreakfast).
But the thing is, I don't use things like this, even in python. How are you expecting to call the functions in that container? "for f in c: try: return f(*misc_args) except: pass"?
-- Imagine that I want to write a program which will help me practice -- basic arithmetic.
-- At its core I might have the following three functions
ask :: (Int, Int) -> String ask (a,b) = show a ++ " + " ++ show b
answer :: (Int, Int) -> Int answer (a,b) = a + b
check :: (Int, Int) -> String -> Bool check q ans = (read ans :: Int) == answer q
-- which present the question, and check whether a given answer is -- correct.
-- Now, imagine I've got addition down pat, and want to extend my -- repertoire to subtraction. I could introduce some flexibility into -- my core functions thus
data Operation = Operation (Int -> Int -> Int) String
ask' :: (Int, Int) -> Operation -> String ask' (a,b) (Operation _ sym) = show a ++ " " ++ sym ++ " " ++ show b
answer' :: (Int, Int) -> Operation -> Int answer' (a,b) (Operation op _) = op a b
check' :: (Int, Int) -> Operation -> String -> Bool check' q op ans = (read ans :: Int) == answer' q op
-- Now my program can deal with any binary infix operations on -- Ints. But what if I now want to practice a unary operation -- (e.g. sqrt)? How about a binary prefix one (e.g. gdc) ?
-- Maybe this is the way forward?
data Question = BinaryInfix (Int -> Int -> Int) String Int Int | BinaryPrefix (Int -> Int -> Int) String Int Int | UnaryPrefix (Int -> Int) String Int
Well, you're creating a little interpreter here. I agree with you that this is sometimes easier in a dynamic language because you can reuse the implementation language at runtime. In the extreme, in python, you can simply call eval() on the input string. I believe there are some packages on hackage that implement little languages that you might be able to reuse. But if you don't need a full-on language, one easy step is to wrap your haskell functions in a typechecker: apply1 f [x] = f x apply1 _ _ = throw hissy fit apply2 f [x, y] = f x y etc. Now you can put them all into one container. Yes, the family of apply functions may be a little tedious, and you may be able to use typeclass magic to automatically select the right apply function, but it doesn't seem like a big deal to me. If you want to extend this to different types, you just have to extend this in one more direction, and a typeclass definitely helps there.
-- I'm a little annoyed by the repetitive tedium of answer'': this -- will really wind me up when I get on to TernaryPrefix, -- QuaternaryPrefix etc. and I will hanker for something like Python's -- *args.
-- Now, I go to a party and thoroughly impress my friends with my -- newly-acquired arithmetic wizardry. One thing leads to another and -- my program ends up in the hands of another soul or two, desperate -- to match my mental calculation powers: I acquire some users. And as -- every schoolboy knows, users are closely followed by feature -- requests.
-- John wants to practice adding fractions. Cindy needs to learn to -- find all prime factors of a given number.
-- Clearly -- -- check'' q a = (read a :: Int) == answer'' q -- -- won't cut the mustard any more.
-- Now, I can't see any obvious reason why I can't just keep adding -- new constructors to Question, and corresponding patterns to ask, -- answer and check, but I'm a lazy bugger and want to palm this off -- onto the users by telling them that I am empowering them by giving -- them the ability to add new question types to the framework.
-- How would I enable them to do this without them having to mess with -- the original source?
Well, I guess you could find the bits of the question framework which are always the same regardless of how its extended, then think about what types those have. Then export that as a library so your users can put together their own program based on that. For example, if you always have a number of wrong answers and a number of right answers and print a scoreboard, then you have 'Int -> Int -> Scoreboard'. If the answers the users are expected to give vary (a single int, or a list of ints, or a string), then you can export some parsing primitives. Eventually, some invisible line is crossed and you have an EDSL for writing math tests. Your Question type could look like 'String -> Answer' and Answer = 'Wrong String | Right | ParseError String'.