Any solution will have to somehow generalize over tuples as by default they're simply disjoint types. The most common solution will use typeclasses to index on the idea of types which "have a first element" such as what Control.Lens
or Data.Tuple.Select
does.
class Sel1 a b | a -> b where sel1 :: a -> b
instance Sel1 (a1,a2) a1 where sel1 (x,_) = x
instance Sel1 (a1,a2,a3) a1 where sel1 (x,_,_) = x
instance Sel1 (a1,a2,a3,a4) a1 where sel1 (x,_,_,_) = x
...
or
instance Field1 (Identity a) (Identity b) a b where
_1 f (Identity a) = Identity <$> indexed f (0 :: Int) a
instance Field1 (a,b) (a',b) a a' where
_1 k ~(a,b) = indexed k (0 :: Int) a <&> \a' -> (a',b)
instance Field1 (a,b,c) (a',b,c) a a' where
_1 k ~(a,b,c) = indexed k (0 :: Int) a <&> \a' -> (a',b,c)
instance Field1 (a,b,c,d) (a',b,c,d) a a' where
_1 k ~(a,b,c,d) = indexed k (0 :: Int) a <&> \a' -> (a',b,c,d)
...
In both cases, consider the type of your function, it'll have to have a way to specify your first argument is a "kind of thing with a first element".
test :: (Sel1 s A) => s -> ...
test :: (Field1 s t A b) => s -> ...
You can also go the route of fixed-vector
and consider tuples as short homogenous vectors. You lose the ability to act on heterogenous vectors, but you gain neat types (but ugly values) like
test :: (Vector v A, Index N1 (Dim v)) => v A -> ...
test v = let (A a) = index (1,2) (undefined :: Z) in ...
though for all its magic it still achieves this work through type classes.