RFC: Derive Struct and Typed instances for structs using GHC.Generics
#540
Closed
RyanGlScott
started this conversation in
General
Replies: 1 comment
-
|
This is now implemented. Thanks, @RyanGlScott ! |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
One of the more tedious aspects of using structs in Copilot is the need to manually define
StructandTypedinstances for each Haskell data type that represents a struct. Each of the instances requires an amount of boilerplate code that is grows linearly with the number of fields in the data type. (For an example of this, see this struct-related example in thecopilotrepo.)I think the experience of using structs would be greatly improved if GHC could automate as much of the process of defining these instances as possible. Luckily, such a thing is possible using
GHC.Generics, and I propose that we make it possible to leverageGHC.Genericsto deriveStructandTypedinstances. (This proposal was inspired the similar work in #516, although this proposal differs from that work slightly.)Recap: the
StructandTypedclassesTo illustrate what this proposal will do, let's use the
Voltsdata type from thecopilotrepo as a running example:copilot/copilot/examples/Structs.hs
Lines 14 to 18 in 068c06d
This is a simple struct-related data type with two fields,
nunVoltsandflag. In order to profitably useVolts, it requires an instance of theStructclass, which looks like this:copilot/copilot/examples/Structs.hs
Lines 20 to 25 in 068c06d
Structhas three methods:typeName: The name of the struct to use in the Copilot-generated code (usually C).toValues: Converts all of theFields toValues.updateField: Dispatches on a particularFieldand updates theValuecontained inside. (Note thatupdateFieldisn't explicitly implemented in the example above, but you can see an example of how one would do it here.)The
toValuesandupdateFieldmethods are extremely mechanical to implement, as their implementations are determined entirely by the type and names of eachField. These are prime candidates for automating viaGHC.Generics.The
typeNamemethod is less mechanical, as it requires a choice on the behalf of the programmer to determine how the struct will be named in the Copilot-generated code. In the example above, the programmer chooses to use the all-lowercase namevoltsinstead of the CamelCase nameVolts. As such, it's unclear if it is possible (or even desirable) to automate the generation of thetypeNamemethod. (We'll return to this later.)Voltsalso requires aTypedinstance, which looks like this:copilot/copilot/examples/Structs.hs
Lines 27 to 29 in 068c06d
Using
GHC.GenericsI propose to design things in such a way that
StructandTypedare (mostly) automated usingGHC.Generics. There are a number of ways that we could go about this, but regardless of which way we pick, we will need to giveVoltsan instance of theGenericclass. This can be done using theDeriveGenericlanguage extension:{-# LANGUAGE DeriveGeneric #-} import GHC.Generics (Generic) data Volts = Volts { numVolts :: Field "numVolts" Word16 , flag :: Field "flag" Bool } deriving GenericOption 1: Explicitly using generic defaults
This option is the least invasive, as it would not require changing anything about the current definitions of the
StructorTypedclasses. The idea is that incopilot-core, we would define "default" versions oftoValues,updateField, andtypeOfwith roughly these type signatures:And then when implementing
StructandTypedinstances for a data type with aGenericinstance, one can simply define them like so:There is still some boilerplate required in defining the instance, but only a constant amount. (Note that we still require the user to implement
typeNamemanually.)Option 2a: Implicitly using generic defaults
We can reduce the amount of boilerplate required if we are willing to change the definitions of the
StructandTypedclasses a bit. Currently, they're defined as:copilot/copilot-core/src/Copilot/Core/Type.hs
Lines 54 to 64 in 068c06d
And:
copilot/copilot-core/src/Copilot/Core/Type.hs
Lines 192 to 197 in 068c06d
Note that some methods (
updateFieldandsimpleType) already have default implementations if you define instances without giving them explicit implementations. As an alternative to option (1), we can change the default implementations so that they use theGeneric-based defaults instead. That is:With these defaults, you can now define
StructandTypedinstances like so:Note that we have changed the default implementation for
updateFieldto require aGenericconstraint, so it is no longer possible to omit anupdateFieldimplementation unless the data type has aGenericinstance. (More on this later.)Option 2b: Using
deriving-related GHC extensionsAdvanced Haskellers will recognize that we don't need to write out the
StructandTypedinstances in a standalone fashion. Instead, we can derive them just as we derive theGenericinstance. To do so, we will need to make use of some additional GHC language extensions that augment thederivingkeyword with extra powers:{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingVia #-} data Volts = Volts { numVolts :: Field "numVolts" Word16 , flag :: Field "flag" Bool } deriving stock Generic deriving anyclass Typed deriving Struct via (GenericStruct "volts" Volts)Now there are no
instancedeclarations anymore: justderiving!How does this all work? Let's go through this bit by bit:
The
DerivingStrategieslanguage extension allows specifying strategies to use with each use of thederivingkeyword. For instance, thestockstrategy is the usual strategy that derivable classes mentioned in the Haskell Report use.(Note that we could just as well leave off the
stockstrategy and writederiving Genericinstead ofderiving stock Generic—they're equivalent ways of writing the same thing. I need to use other deriving strategies elsewhere in this program, however, so I decided to be explicit here about which deriving strategy is in use.)The
DerivingAnyClassandDerivingVialanguage extensions allow using theanyclassandviaderiving strategies.Writing
deriving anyclass Typedderives aTypedinstance as though you had written a separateinstance Typed Voltsdeclaration (using default implementations for all methods). Again, we could technically leave off theanyclassstrategy here, but I decided to be explicit.deriving Struct via (GenericStruct "volts" Volts)is the most interesting part. In order for this to work,copilotneeds to offer aGenericStructnewtype:This newtype should also come equipped with a
Structinstance that leveragesGeneric-based defaults:Now, one can use
DerivingViato derive aStructinstance that reuses the existingStructinstance forGenericStruct. This works becauseVoltsandGenericStruct s Voltshave the same underlying representation. ThesinGenericStruct sspecifies howtypeNameshould be implemented, and this is the only place where the programmer has to make a choice.Note that this approach (option 2b) is fully compatible with option 2a above, as both options are different syntaxes for accomplishing the same thing. As such, advanced Haskellers can use this approach if they want, but if the use of
deriving-related GHC extensions is too much, one can always fall back to the (comparatively less advanced) approach used in option 2a. I'll collectively refer to both option 2a and 2b as "option 2".Option 1 or 2?
As noted above, option 1 does not require any changes to the defaults in the
StructandTypedclasses, while option 2 does. As such, option 2 requires a backwards-compatible API change, as there would be existingStruct/Typedinstances that would no longer compile unless the user added aGenericinstance to their struct types.Personally, I am in favor of option 2, even with the need for an API change. Given how simple it is to derive a
Genericinstance, migrating existing code to the new defaults should be very straightforward. As such, I think this is an acceptable price to pay.Beta Was this translation helpful? Give feedback.
All reactions