I would argue that in this case existential types actually are the correct tool. What you want to do is hide some amount of type information, which is exactly what existential types do. Then, because createTable can handle any Table a when you unwrap the Table from the existential type you can still pass it to createTable.
Here's a sort of mock example:
```{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad (forM_)
data Table a
data a :*: b where
(:*:) :: a -> b -> a :*: b
infixr 1 :*:
type RowID = Int
type Text = String
categories :: Table (RowID :*: Text)
categories = undefined
expenses :: Table (RowID:*:Text:*:Double:*:RowID)
expenses = undefined
createTable :: Table a -> IO ()
createTable _ = return ()
data ExTable = forall a. ExTable (Table a)
main :: IO ()
main = forM_ [ExTable categories, ExTable expenses] (\case ExTable t -> createTable t)
```
In your example this requires more boilerplate and doesn't seem much better than [createTable categories, createTable expenses], but this provides a way to actually have a list of tables of differing types without applying createTable to them first and I think that's closer to what you were going for.