Description
For each record type e.g. Get_foo
that represents a set of query parameters, the generator also produces a function mkGet_foo
whose parameters are only the values of Get_foo
that are required. This can be nice compared to using the Get_foo
constructor directly because when an API adds new optional query parameters to a resource, that doesn't have to be a breaking change to users of the client. However, we've found some drawbacks:
- Since
mkGet_foo
uses positional arguments rather than named record fields, it's easy to accidentally transpose the arguments if they are of the same type e.g.Text
. - We've had one bug where we constructed a value using
mkGet_foo
and immediately followed by aGet_foo
record update that overwrote one of the required fields thatmkGet_foo
had set. There's no indication in the types and no easy way from looking at the user code to see whichmkGet_foo
parameters correspond to whichGet_foo
record fields.
I'd like to throw out an idea:
-
For each
Get_foo
type, generate two corresponding record typesRequired_Get_foo
Optional_Get_foo
which divide up the required and optional parameters respectively
-
Change
mkGet_foo
(or add a different function) to build aGet_foo
from aRequired_Get_foo
and anOptional_Get_foo
. -
Define a constant
Optional_Get_foo
that hasNothing
for all the fields, any of the following would work:- a separate named constant for each parameter set;
mempty
(if a semigroup operation is also wanted)- def
I think both
Monoid
andDefault
have the advantage that they can be derived with generics rather than writing yet more template haskell.
So parameter construction then could go from
parameters :: Get_foo
parameters = (mkGet_foo a b){ someOptionalParameter = c }
to:
parameters :: Get_foo
parameters =
mkGet_foo'
Required_Get_foo
{ requiredParameter = a
, anotherRequiredParameter = b
}
$ def
{ someOptionalParameter = c
}
That way we have the advantages of both the mk
function and of record construction:
- All the parameters are given explicitly by name.
- Adding optional parameters doesn't cause a breaking change at the use site.
One additional thought: To limit the explosion in the number of new generated identifiers introduced, a type class with an associated data family might be a nice way to go about it.
class Parameters a where
data RequiredParameters a :: Type
data OptionalParameters a :: Type
defaultOptionalParameters :: OptionalParameters a
mkParameters :: RequiredParameters a -> OptionalParameters a -> a
Then adding support for this feature is mostly just a matter of generating Parameters
instances.