{{ message }}
Add derive clauses to data and newtype declarations#4592
Closed
i-am-the-slime wants to merge 1 commit into
Closed
Conversation
8bb7180 to
04c68ee
Compare
This was referenced Apr 3, 2026
7568d53 to
acb5ae4
Compare
acb5ae4 to
4cd20df
Compare
4cd20df to
30b21c5
Compare
30b21c5 to
7704963
Compare
i-am-the-slime
commented
Apr 4, 2026
i-am-the-slime
left a comment
Contributor
Author
There was a problem hiding this comment.
Notes on the less obvious parts.
| _ -> pure decl | ||
| DeriveClause sa _ddt tyName tyVars className extraArgs DerivedInstance | ||
| | className == Libs.Generic || className == Libs.Newtype -> | ||
| deriveInstance mn ds (expandDeriveClause mn sa tyName tyVars className extraArgs DerivedInstance) |
Contributor
Author
There was a problem hiding this comment.
Generic and Newtype derive clauses get expanded early here because deriveInstance needs to fill in the wildcard (the representation type) by inspecting the data constructors. That has to happen before the general DeriveClause expansion in TypeClasses.hs.
| -> [SourceConstraint] | ||
| -> [FunctionalDependency] | ||
| -> m Expr | ||
| deriveViaInstance mn className tys viaTy _typeClassArguments _typeClassSuperclasses _typeClassDependencies = do |
Contributor
Author
There was a problem hiding this comment.
Validates three things before emitting a DeferredDictionary that swaps the instance's last type arg with the via type:
- No floating type variables in the via type
- The via type and the actual instance type have the same runtime representation
- Superclass instances exist or can be verified
| _ -> internalError "typesOf did not return a singleton" | ||
| go ValueDeclaration{} = internalError "Binders were not desugared" | ||
| go BoundValueDeclaration{} = internalError "BoundValueDeclaration should be desugared" | ||
| go DeriveClause{} = internalError "DeriveClause should be desugared" |
Contributor
Author
There was a problem hiding this comment.
DeriveClause is always desugared before type checking, but GHC's -Wincomplete-patterns with -Werror (used in CI via stack) requires the case.
e4eee71 to
9235455
Compare
9235455 to
7134ec2
Compare
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What this enables
Derive clauses can now be written directly on type declarations:
Newtype deriving and deriving via work too:
Multiple classes per clause, multiple clauses per type, and you can mix with standalone
derive instancefreely.Why
Long-requested: #3426, #1891. The standalone
derive instancesyntax gets noisy fast, #3426 has a good side-by-side showing how 4 simple enum types balloon into 28 instance declarations.Prior art
This PR also includes standalone
derive via, building on @kl0tl's work in #3824 (2020). That PR mapped out the key error cases, floating type variables, kind mismatches, non-coercible via types, superclass verification, and I handle the same cases here.I diverged from #3824 in a few places: I kept
viacontext-sensitive (not a keyword token) so it still works as a variable name, addedViaInstanceas its own constructor rather than modifyingDerivedInstance, and preserved existing error names instead of renaming them.The deriving clause syntax itself is new and not part of #3824.
Closes #3426.
Implementation
This is a desugaring pass. Deriving clauses get parsed into a temporary
DeriveClauseAST node, then expanded into regularTypeInstanceDeclarations during type class desugaring. Everything downstream — type checker, deriving solver, codegen — sees ordinary instance declarations and doesn't need to change.Design choices
deriveandviaare context-sensitive — neither is a reserved word, so existing code using them as identifiers or record labels is unaffected.derivenotderiving— mirrors PS's existing keyword (derive instance).Strategy placement mirrors standalone syntax —
derive newtype (Eq)matchesderive newtype instance Eq T, andderive (Show) via Tkeepsviaattached to the clause rather than the class.Fundep-aware argument inference — for multi-param classes where all parameters after the first are determined by functional dependencies (like
Generic a rep | a -> rep), wildcards are filled in automatically.Shortcomings and future work
No constraint inference. For parameterized types,
derive (Eq)ondata Pair a = Pair a ageneratesderive instance Eq (Pair a)which fails — there's noEq aconstraint. This is the same limitation standalone derive has; the workaround is the same (use standalone syntax with an explicit constraint). A future improvement could infer constraints as GHC does.Multi-param classes without fundeps need explicit args. Classes with 2+ parameters where the extra params aren't determined by functional dependencies require you to spell out the type arguments.
Open design question:
viaplacement in standalone syntaxThe standalone
derive viasyntax currently puts the strategy beforeinstance, matching Haskell and the existingderive newtype instancepattern:An alternative would be to always put
vialast, and unifynewtypeasvia newtype:This is more uniform —
viaalways means "how to derive" and always comes at the end — but it would be a breaking change to the existingderive newtype instancesyntax. Feedback welcome on which direction to go.