Yes, though not in Haskell. But the higher-order polymorphic lambda calculus (aka System F-omega) is more general:
bi : forall m n a b. (forall a. m a -> n a) -> m a -> m b -> (n a, n b)
bi {m} {n} {a} {b} f x y = (f {a} x, f {b} y)
x1 : (Integer, Char)
x1 = bi {\a. List a} {\a. a} {Integer} {Char} head [2,3] "45"
x2 : (Integer, Char)
x2 = bi {\a . exists b. (a, b)} {\a. a} {Integer} {Char} (\{a}. \p. unpack<b,x>=p in fst {a} {b} x) (pack<Char, (2,'3')>) (pack<Integer, ('4',5)>)
x3 : (Integer, Double)
x3 = bi {\a. a} {\a. a} {Integer} {Double} (1+) 2 3.45
Here, I write f {T}
for explicit type application and assume a library typed respectively. Something like \a. a
is a type-level lambda. The x2
example is more intricate, because it also needs existential types to locally "forget" the other bit of polymorphism in the arguments.
You can actually simulate this in Haskell by defining a newtype
or datatype for every different m
or n
you instantiate with, and pass appropriately wrapped functions f
that add and remove constructors accordingly. But obviously, that's no fun at all.
Edit: I should point out that this still isn't a fully general solution. For example, I can't see how you could type
swap (x,y) = (y,x)
x4 = bi swap (3, "hi") (True, 3.1)
even in System F-omega. The problem is that the swap
function is more polymorphic than bi
allows, and unlike with x2
, the other polymorphic dimension is not forgotten in the result, so the existential trick does not work. It seems that you would need kind polymorphism to allow that one (so that the argument to bi
can be polymorphic over a varying number of types).