2
votes

I'm writing up some CRUD helpers for fun and profit, and I'm finding myself needing an empty or noop route. The mempty to :>, if you will.

This is what I'd like to write:

type Index model =
  Reassoc (QueryParams model :> Get '[JSON] (Collection model))

type family Reassoc xs where
  Reassoc ((x :> y) :> z) = Reassoc (x :> Reassoc (y :> z))
  Reassoc (x :> y) = x :> y

type family QueryParams model

type instance QueryParams User =
  MakeQueryParams '[ '("organizationId", Int) ]

That all of course builds up to this guy:

type family MakeQueryParams xs where
  MakeQueryParams ( '(sym, ty) ': xs ) 
    = QueryParam sym ty :> MakeQueryParams xs
  MakeQueryParams '[] 
    = ... :(

Is there an empty route combinator?

I've worked around this thus far by using a next parameter in those families, but it's a lot less idiomatic for Servant.

type family MakeQueryParams xs next where
    MakeQueryParams '[] next =
        next
    MakeQueryParams ('(sym, ty) ': xs) next =
        QueryParam sym ty :> MakeQueryParams xs next

type Index model = QueryParams model (Get '[JSON] (Collection model))

type family QueryParams model next

type instance QueryParams User next =
    MakeQueryParams '[ '("organizationId", Int) ] next
1
I'd say the version with next is great and perfectly idiomatic (or at least much more than your other attempt). You don't need Reassoc. I'd also argue that something like (QueryParam ... :> QueryParam ...) :> Get ... shouldn't even be kind-correct. The fact that it currently is is an accident / compromise.kosmikus

1 Answers

0
votes

if you really insist on writing

type API = QueryParams '[ '("id", Int) ] :> Get '[JSON] Bool

you combining your foldr like idea/solution (which is completely fine) with a new combinator:

data QueryParams (ps :: [(Symbol, *)])

instance HasServer api ctx
  => HasServer (QueryParams '[] :> api) ctx where
    type ServerT (QueryParams '[] api) m = ServerT api m

    route = ...

instance HasServer (QueryParam sym ty :> MakeQueryParams ('(sym, ty) ': ps) api) ctx
  => HasServer (QueryParams ('(sym, ty) ': ps) api) ctx where
    type ServerT (QueryParams ('(sym, ty) ': ps) api) m = ...

    route = ...

Writing separate instance for nil and cons cases will make implementation of instances more direct.

One can think that we have to introduce new combinator, as otherwise we cannot plug type family in the right place. A bit like we sometimes need to write newtypes to write different instances.