The difficulty with doing this in a statically typed system is that once you've fixed the type of the function you pass in, you can't then apply it to two different types. The standard way to solve this would be to specify that the function you pass in is valid for all types [1]
f :: (forall s. s -> s) -> a -> b -> (a,b) -- [2]
f r a b = (r a, r b)
f id 5 "Test" -- returns (5,"Test")
However, the challenge also specified that you do it without writing a type signature for `f`. I admit that in this case I am pretty stumped. The closest I can get is to first define a wrapper data S = forall a. Show a => S a
instance Show S where Show (S a) = show a
and then write f r a b = (r a, r b)
f id (S 5) (S "Test") -- returns (5,"Test")
but that involves wrapping everything up before passing it into the function, to disguise the fact that we really have different types.There's an interesting point to be made here, though - "wrapping everything up in one type" is essentially what dynamic type systems do, except that it's baked into the language instead of being implemented by the programmer on an as-needed basis.
Here, my declaration for the type S says "any type as long as it has a Show instance" (which allows it to be printed). I could instead have specified "any type that has a Num instance" which would mean that I could pass in a mix of ints and floats, and still do arithmetic with them. I get some of the benefits of dynamic typing (flexibility) but I still get to keep type checking.
The Python approach is "any type, and we'll check at runtime that it has the appropriate methods". So it's ultimately flexible, but as a result loses any form of compile-time checking.
Looking at it this way, I think that the challenge you've set me is one that cuts to the core of the difference between dynamic and static type systems. No, I can't write the function you want in a statically typed language without doing something clever with types. But the clever thing I have to do with types is re-invent duck typing, but in a limited way which still gives me compile-time checking.
Anyhow, thanks for the insightful comment. It certainly made me think...
[1] This requires the language extensions ExistentialQuantification and Rank2Types, but neither of those are particularly controversial.
[2] Note that the type signature
forall a. a -> a
is inhabited by exactly one function - the identity function. This carries over to the Python example too - the function `f` isn't safe to use unless the
first argument is the identity function, which makes it a bit of a useless function to define in the first place.