Hasql: inserting many values that aren't tuples

Params is an instance of Divisible, which has (morally) the operation you want: divide :: (a -> (b, c)) -> f b -> f c -> f a.

Unfortunately, I don’t know of any good story for making this primitive work smoothly with records. I’d normally point at George Wilson’s talk, Contravariant functors: The other side of the coin, but YOW seems to have removed all their Lambda Jam videos and not re-uploaded all of them to the main channel. It provided reasonably usable operators (>*), (>*<), (*<), (>|), (>|<), and (|<). Some discussion on the contravariant issue tracker might give you the flavour.

A haskellforall blog post also proposes an (>*<) operator but needs helper adapt :: Record -> (Field1, (Field2, (Field3, Field4))) functions to get any useful work done. The comments section offers a solution using Generic, but that’s not much better.

I tried using a HKD record but the barbies library didn’t provide the typeclass I needed: you need a version of TraversableB that combines results using divide instead of (<*>). There’s an issue on the contravariant bugtracker about a possible class Contraverse; adapting that to barbies naming and idioms gives something reasonable (the ContraversableB instance is probably derivable using generics):

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

import Data.Functor.Barbie (FunctorB (..), Rec (..))
import Data.Functor.Compose (Compose (..))
import Data.Functor.Contravariant (Contravariant (..))
import Data.Functor.Contravariant.Divisible (Divisible (..), divided)
import Data.Functor.Identity (Identity (..))
import GHC.Generics (Generic)

-- machinery that could (should?) exist in `barbies`:
class (FunctorB b) => ContraversableB b where
  bcontraverse :: (Divisible e) => (forall a. f a -> e (g a)) -> b f -> e (b g)

bconsequence :: (ContraversableB b, Divisible e) => b (Compose e g) -> e (b g)
bconsequence = bcontraverse getCompose

bconsequence' :: (ContraversableB b, Divisible e) => b e -> e (b Identity)
bconsequence' = bcontraverse (contramap runIdentity)

-- mockup of the `Param` type, with sample encoders:
data Param a

instance Contravariant Param

instance Divisible Param

pInt :: Param Int
pInt = undefined

pBool :: Param Bool
pBool = undefined

pChar :: Param Char
pChar = undefined

pString :: Param String
pString = undefined

-- HKD record:
data MyRecord f = MyRecord
  { f1 :: f Int,
    f2 :: f Bool,
    f3 :: f Char,
    f4 :: f String
  }
  deriving stock (Generic)
  deriving anyclass (FunctorB)

instance ContraversableB MyRecord where
  bcontraverse nt r =
    divide adapt (nt (f1 r))
      . divided (nt (f2 r))
      $ divided (nt (f3 r)) (nt (f4 r))
    where
      adapt (MyRecord {f1, f2, f3, f4}) = (f1, (f2, (f3, f4)))

myRecordParam :: Param (MyRecord Identity)
myRecordParam =
  bconsequence'
    MyRecord
      { f1 = pInt,
        f2 = pBool,
        f3 = pChar,
        f4 = pString
      }