You could have both abstractions and DRY without mixing it in imperative library calls and opaque state. You could then write eg tools that apply changes robustly across all your stacks, inspect for security risks, check against custom schemas and policies, reusable things that compose better, etc.
I'm doing all of that actually... I don't see how "functional" and "declarative" is in any way helping... You're making statements without getting into the details.
For instance we run a linter for security issues and best practices during the cdk build... We have tools applying changes across our abstractions and against our non-abstractions.
In a declarative form the changes don't make it back into the declarative form unless you're pullling cfn state back into say git, or harder reversing it back into TFN state.
Think for example how you would robustly check that resources of certain types are labeled in a way conforming to a given schema, or that would rewrite the label values in your stacks to a newer format. Linting CDK code can't get you this.
It's possible of course that I don't know enough about CDK and these examples have CDK solutions.
Imperative and declarative are not hard requirements for this but they make the domain easier to reason about and they are a good fit to the CF model of configuration-as-data instead (vs CDK's configuration-as-imperative-code).
Cdk linters can 100% do this. They can operate at the various construct levels including level 1 eg generated cloud formation. At this point it's just making a linter/visitor that says "if type is taggable then it must have this tag" and bob's your uncle. The l1 stuff is generated from the cfn schema and so will have tags correctly.