Skip to content

There is a better way to parse record fields (ApplicativeDo + RecordWildCards)#12107

Open
Bodigrim wants to merge 1 commit into
masterfrom
applicative-do
Open

There is a better way to parse record fields (ApplicativeDo + RecordWildCards)#12107
Bodigrim wants to merge 1 commit into
masterfrom
applicative-do

Conversation

@Bodigrim

@Bodigrim Bodigrim commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Parsing of arguments in Cabal is typically organised as a humongous sequence of (<*>). Here is an example:

packageConfigFieldGrammar :: [String] -> ParsecFieldGrammar' PackageConfig
packageConfigFieldGrammar knownPrograms =
mkPackageConfig
<$> optionalFieldDef "haddock-all" noopLens mempty
^^^ hiddenField
<*> pure mempty -- program-options stanza
<*> pure mempty -- program-locations stanza
<*> monoidalFieldAla "extra-prog-path" (alaNubList' FSep FilePathNT) L.packageConfigProgramPathExtra
<*> monoidalField "flags" L.packageConfigFlagAssignment
<*> optionalFieldDef "library-vanilla" L.packageConfigVanillaLib mempty
<*> optionalFieldDef "shared" L.packageConfigSharedLib mempty
<*> optionalFieldDef "static" L.packageConfigStaticLib mempty
<*> optionalFieldDef "library-bytecode" L.packageConfigBytecodeLib mempty
<*> optionalFieldDef "executable-dynamic" L.packageConfigDynExe mempty
<*> optionalFieldDef "executable-static" L.packageConfigFullyStaticExe mempty
<*> optionalFieldDef "profiling" L.packageConfigProf mempty
<*> optionalFieldDef "library-profiling" L.packageConfigProfLib mempty
<*> optionalFieldDef "profiling-shared" L.packageConfigProfShared mempty
<*> optionalFieldDef "executable-profiling" L.packageConfigProfExe mempty
<*> optionalFieldDef "profiling-detail" L.packageConfigProfDetail mempty
<*> optionalFieldDef "library-profiling-detail" L.packageConfigProfLibDetail mempty
<*> monoidalFieldAla "configure-options" (alaList' NoCommaFSep Token) L.packageConfigConfigureArgs
<*> optionalFieldDef "optimization" L.packageConfigOptimization mempty
<*> optionalFieldDef "program-prefix" L.packageConfigProgPrefix mempty
<*> optionalFieldDef "program-suffix" L.packageConfigProgSuffix mempty
<*> monoidalFieldAla "extra-lib-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraLibDirs
<*> monoidalFieldAla "extra-lib-dirs-static" (alaList' FSep FilePathNT) L.packageConfigExtraLibDirsStatic
<*> monoidalFieldAla "extra-framework-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraFrameworkDirs
<*> monoidalFieldAla "extra-include-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraIncludeDirs
<*> optionalFieldDef "library-for-ghci" L.packageConfigGHCiLib mempty
<*> optionalFieldDef "split-sections" L.packageConfigSplitSections mempty
<*> optionalFieldDef "split-objs" L.packageConfigSplitObjs mempty
<*> optionalFieldDef "executable-stripping" L.packageConfigStripExes mempty
<*> optionalFieldDef "library-stripping" L.packageConfigStripLibs mempty
<*> optionalFieldDef "tests" L.packageConfigTests mempty
<*> optionalFieldDef "benchmarks" L.packageConfigBenchmarks mempty
<*> packageConfigCoverageGrammar
<*> optionalFieldDef "relocatable" L.packageConfigRelocatable mempty
<*> optionalFieldDef "debug-info" L.packageConfigDebugInfo mempty
<*> optionalFieldDef "build-info" L.packageConfigDumpBuildInfo mempty
<*> optionalFieldDef "run-tests" L.packageConfigRunTests mempty
<*> optionalFieldDef "documentation" L.packageConfigDocumentation mempty
<*> optionalFieldDef "haddock-hoogle" L.packageConfigHaddockHoogle mempty
<*> optionalFieldDef "haddock-html" L.packageConfigHaddockHtml mempty
<*> optionalFieldDefAla "haddock-html-location" (alaFlag Token) L.packageConfigHaddockHtmlLocation mempty
<*> optionalFieldDef "haddock-foreign-libraries" L.packageConfigHaddockForeignLibs mempty
<*> optionalFieldDef "haddock-executables" L.packageConfigHaddockExecutables mempty
<*> optionalFieldDef "haddock-tests" L.packageConfigHaddockTestSuites mempty
<*> optionalFieldDef "haddock-benchmarks" L.packageConfigHaddockBenchmarks mempty
<*> optionalFieldDef "haddock-internal" L.packageConfigHaddockInternal mempty
<*> optionalFieldDefAla "haddock-css" (alaFlag FilePathNT) L.packageConfigHaddockCss mempty
<*> optionalFieldDef "haddock-hyperlink-source" L.packageConfigHaddockLinkedSource mempty
<*> optionalFieldDef "haddock-quickjump" L.packageConfigHaddockQuickJump mempty
<*> optionalFieldDefAla "haddock-hscolour-css" (alaFlag FilePathNT) L.packageConfigHaddockHscolourCss mempty
<*> optionalFieldDef "haddock-contents-location" L.packageConfigHaddockContents mempty
<*> optionalFieldDef "haddock-index-location" L.packageConfigHaddockIndex mempty
<*> optionalFieldDefAla "haddock-base-url" (alaFlag Token) L.packageConfigHaddockBaseUrl mempty
<*> optionalFieldDefAla "haddock-resources-dir" (alaFlag Token) L.packageConfigHaddockResourcesDir mempty
<*> optionalFieldDefAla "haddock-output-dir" (alaFlag FilePathNT) L.packageConfigHaddockOutputDir mempty
<*> optionalFieldDef "haddock-use-unicode" L.packageConfigHaddockUseUnicode mempty
<*> optionalFieldDef "haddock-for-hackage" L.packageConfigHaddockForHackage mempty
<*> optionalFieldDef "test-log" L.packageConfigTestHumanLog mempty
<*> optionalFieldDef "test-machine-log" L.packageConfigTestMachineLog mempty
<*> optionalFieldDef "test-show-details" L.packageConfigTestShowDetails mempty
<*> optionalFieldDef "test-keep-tix-files" L.packageConfigTestKeepTix mempty
<*> optionalFieldDefAla "test-wrapper" (alaFlag FilePathNT) L.packageConfigTestWrapper mempty
<*> optionalFieldDef "test-fail-when-no-test-suites" L.packageConfigTestFailWhenNoTestSuites mempty
<*> monoidalFieldAla "test-options" (alaList NoCommaFSep) L.packageConfigTestTestOptions
<*> monoidalFieldAla "benchmark-options" (alaList NoCommaFSep) L.packageConfigBenchmarkOptions
-- A PackageConfig may contain -options and -location fields inside a package * (projectConfigAllPackages) or package <name> stanza (packageConfigSpecificPackage).
-- When declared at top level (packageConfigLocalPackages), the PackageConfig must contain a program-options stanza/program-locations for these fields.
<* traverse_ (knownField . BS.pack . (<> "-options")) knownPrograms
<* traverse_ (knownField . BS.pack . (<> "-location")) knownPrograms
where
noopLens f s = s <$ f mempty
mkPackageConfig
haddockAll
packageConfigProgramPaths
packageConfigProgramArgs
packageConfigProgramPathExtra
packageConfigFlagAssignment
packageConfigVanillaLib
packageConfigSharedLib
packageConfigStaticLib
packageConfigBytecodeLib
packageConfigDynExe
packageConfigFullyStaticExe
packageConfigProf
packageConfigProfLib
packageConfigProfShared
packageConfigProfExe
packageConfigProfDetail
packageConfigProfLibDetail
packageConfigConfigureArgs
packageConfigOptimization
packageConfigProgPrefix
packageConfigProgSuffix
packageConfigExtraLibDirs
packageConfigExtraLibDirsStatic
packageConfigExtraFrameworkDirs
packageConfigExtraIncludeDirs
packageConfigGHCiLib
packageConfigSplitSections
packageConfigSplitObjs
packageConfigStripExes
packageConfigStripLibs
packageConfigTests
packageConfigBenchmarks
packageConfigCoverage
packageConfigRelocatable
packageConfigDebugInfo
packageConfigDumpBuildInfo
packageConfigRunTests
packageConfigDocumentation
packageConfigHaddockHoogle
packageConfigHaddockHtml
packageConfigHaddockHtmlLocation
packageConfigHaddockForeignLibs'
packageConfigHaddockExecutables'
packageConfigHaddockTestSuites'
packageConfigHaddockBenchmarks'
packageConfigHaddockInternal
packageConfigHaddockCss
packageConfigHaddockLinkedSource
packageConfigHaddockQuickJump
packageConfigHaddockHscolourCss
packageConfigHaddockContents
packageConfigHaddockIndex
packageConfigHaddockBaseUrl
packageConfigHaddockResourcesDir
packageConfigHaddockOutputDir
packageConfigHaddockUseUnicode
packageConfigHaddockForHackage
packageConfigTestHumanLog
packageConfigTestMachineLog
packageConfigTestShowDetails
packageConfigTestKeepTix
packageConfigTestWrapper
packageConfigTestFailWhenNoTestSuites
packageConfigTestTestOptions
packageConfigBenchmarkOptions =
PackageConfig
{ -- The haddock-al` field provides a default value, but explicit declarations can override it
packageConfigHaddockForeignLibs = haddockAll <> packageConfigHaddockForeignLibs'
, packageConfigHaddockExecutables = haddockAll <> packageConfigHaddockExecutables'
, packageConfigHaddockTestSuites = haddockAll <> packageConfigHaddockTestSuites'
, packageConfigHaddockBenchmarks = haddockAll <> packageConfigHaddockBenchmarks'
, ..
}

This is inconvenient to read and error prone to write. Half of the fields here are of the same type Flag Bool, so one can easily swap two of them and never notice a mistake. If you remove a field of PackageConfig, you'll be presented with a very long error message saying that it cannot apply, say, 30th argument out of 60+ of them. If you add a field to PackageConfig - same story, the error message will talk about function of 60+ arguments (aka constructor) having a wrong type at some position. This is not a sane way to work with records.

There is a known pattern of ApplicativeDo + RecordWildCards which fixes these issues. Here is an updated example:

packageConfigFieldGrammar :: [String] -> ParsecFieldGrammar' PackageConfig
packageConfigFieldGrammar knownPrograms = do
haddockAll <-
optionalFieldDef "haddock-all" noopLens mempty
^^^ hiddenField
let packageConfigProgramPaths = mempty
packageConfigProgramArgs = mempty
packageConfigProgramPathExtra <- monoidalFieldAla "extra-prog-path" (alaNubList' FSep FilePathNT) L.packageConfigProgramPathExtra
packageConfigFlagAssignment <- monoidalField "flags" L.packageConfigFlagAssignment
packageConfigVanillaLib <- optionalFieldDef "library-vanilla" L.packageConfigVanillaLib mempty
packageConfigSharedLib <- optionalFieldDef "shared" L.packageConfigSharedLib mempty
packageConfigStaticLib <- optionalFieldDef "static" L.packageConfigStaticLib mempty
packageConfigBytecodeLib <- optionalFieldDef "library-bytecode" L.packageConfigBytecodeLib mempty
packageConfigDynExe <- optionalFieldDef "executable-dynamic" L.packageConfigDynExe mempty
packageConfigFullyStaticExe <- optionalFieldDef "executable-static" L.packageConfigFullyStaticExe mempty
packageConfigProf <- optionalFieldDef "profiling" L.packageConfigProf mempty
packageConfigProfLib <- optionalFieldDef "library-profiling" L.packageConfigProfLib mempty
packageConfigProfShared <- optionalFieldDef "profiling-shared" L.packageConfigProfShared mempty
packageConfigProfExe <- optionalFieldDef "executable-profiling" L.packageConfigProfExe mempty
packageConfigProfDetail <- optionalFieldDef "profiling-detail" L.packageConfigProfDetail mempty
packageConfigProfLibDetail <- optionalFieldDef "library-profiling-detail" L.packageConfigProfLibDetail mempty
packageConfigConfigureArgs <- monoidalFieldAla "configure-options" (alaList' NoCommaFSep Token) L.packageConfigConfigureArgs
packageConfigOptimization <- optionalFieldDef "optimization" L.packageConfigOptimization mempty
packageConfigProgPrefix <- optionalFieldDef "program-prefix" L.packageConfigProgPrefix mempty
packageConfigProgSuffix <- optionalFieldDef "program-suffix" L.packageConfigProgSuffix mempty
packageConfigExtraLibDirs <- monoidalFieldAla "extra-lib-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraLibDirs
packageConfigExtraLibDirsStatic <- monoidalFieldAla "extra-lib-dirs-static" (alaList' FSep FilePathNT) L.packageConfigExtraLibDirsStatic
packageConfigExtraFrameworkDirs <- monoidalFieldAla "extra-framework-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraFrameworkDirs
packageConfigExtraIncludeDirs <- monoidalFieldAla "extra-include-dirs" (alaList' FSep FilePathNT) L.packageConfigExtraIncludeDirs
packageConfigGHCiLib <- optionalFieldDef "library-for-ghci" L.packageConfigGHCiLib mempty
packageConfigSplitSections <- optionalFieldDef "split-sections" L.packageConfigSplitSections mempty
packageConfigSplitObjs <- optionalFieldDef "split-objs" L.packageConfigSplitObjs mempty
packageConfigStripExes <- optionalFieldDef "executable-stripping" L.packageConfigStripExes mempty
packageConfigStripLibs <- optionalFieldDef "library-stripping" L.packageConfigStripLibs mempty
packageConfigTests <- optionalFieldDef "tests" L.packageConfigTests mempty
packageConfigBenchmarks <- optionalFieldDef "benchmarks" L.packageConfigBenchmarks mempty
packageConfigCoverage <- packageConfigCoverageGrammar
packageConfigRelocatable <- optionalFieldDef "relocatable" L.packageConfigRelocatable mempty
packageConfigDebugInfo <- optionalFieldDef "debug-info" L.packageConfigDebugInfo mempty
packageConfigDumpBuildInfo <- optionalFieldDef "build-info" L.packageConfigDumpBuildInfo mempty
packageConfigRunTests <- optionalFieldDef "run-tests" L.packageConfigRunTests mempty
packageConfigDocumentation <- optionalFieldDef "documentation" L.packageConfigDocumentation mempty
packageConfigHaddockHoogle <- optionalFieldDef "haddock-hoogle" L.packageConfigHaddockHoogle mempty
packageConfigHaddockHtml <- optionalFieldDef "haddock-html" L.packageConfigHaddockHtml mempty
packageConfigHaddockHtmlLocation <- optionalFieldDefAla "haddock-html-location" (alaFlag Token) L.packageConfigHaddockHtmlLocation mempty
packageConfigHaddockForeignLibs' <- optionalFieldDef "haddock-foreign-libraries" L.packageConfigHaddockForeignLibs mempty
packageConfigHaddockExecutables' <- optionalFieldDef "haddock-executables" L.packageConfigHaddockExecutables mempty
packageConfigHaddockTestSuites' <- optionalFieldDef "haddock-tests" L.packageConfigHaddockTestSuites mempty
packageConfigHaddockBenchmarks' <- optionalFieldDef "haddock-benchmarks" L.packageConfigHaddockBenchmarks mempty
packageConfigHaddockInternal <- optionalFieldDef "haddock-internal" L.packageConfigHaddockInternal mempty
packageConfigHaddockCss <- optionalFieldDefAla "haddock-css" (alaFlag FilePathNT) L.packageConfigHaddockCss mempty
packageConfigHaddockLinkedSource <- optionalFieldDef "haddock-hyperlink-source" L.packageConfigHaddockLinkedSource mempty
packageConfigHaddockQuickJump <- optionalFieldDef "haddock-quickjump" L.packageConfigHaddockQuickJump mempty
packageConfigHaddockHscolourCss <- optionalFieldDefAla "haddock-hscolour-css" (alaFlag FilePathNT) L.packageConfigHaddockHscolourCss mempty
packageConfigHaddockContents <- optionalFieldDef "haddock-contents-location" L.packageConfigHaddockContents mempty
packageConfigHaddockIndex <- optionalFieldDef "haddock-index-location" L.packageConfigHaddockIndex mempty
packageConfigHaddockBaseUrl <- optionalFieldDefAla "haddock-base-url" (alaFlag Token) L.packageConfigHaddockBaseUrl mempty
packageConfigHaddockResourcesDir <- optionalFieldDefAla "haddock-resources-dir" (alaFlag Token) L.packageConfigHaddockResourcesDir mempty
packageConfigHaddockOutputDir <- optionalFieldDefAla "haddock-output-dir" (alaFlag FilePathNT) L.packageConfigHaddockOutputDir mempty
packageConfigHaddockUseUnicode <- optionalFieldDef "haddock-use-unicode" L.packageConfigHaddockUseUnicode mempty
packageConfigHaddockForHackage <- optionalFieldDef "haddock-for-hackage" L.packageConfigHaddockForHackage mempty
packageConfigTestHumanLog <- optionalFieldDef "test-log" L.packageConfigTestHumanLog mempty
packageConfigTestMachineLog <- optionalFieldDef "test-machine-log" L.packageConfigTestMachineLog mempty
packageConfigTestShowDetails <- optionalFieldDef "test-show-details" L.packageConfigTestShowDetails mempty
packageConfigTestKeepTix <- optionalFieldDef "test-keep-tix-files" L.packageConfigTestKeepTix mempty
packageConfigTestWrapper <- optionalFieldDefAla "test-wrapper" (alaFlag FilePathNT) L.packageConfigTestWrapper mempty
packageConfigTestFailWhenNoTestSuites <- optionalFieldDef "test-fail-when-no-test-suites" L.packageConfigTestFailWhenNoTestSuites mempty
packageConfigTestTestOptions <- monoidalFieldAla "test-options" (alaList NoCommaFSep) L.packageConfigTestTestOptions
packageConfigBenchmarkOptions <- monoidalFieldAla "benchmark-options" (alaList NoCommaFSep) L.packageConfigBenchmarkOptions
-- A PackageConfig may contain -options and -location fields inside a package * (projectConfigAllPackages) or package <name> stanza (packageConfigSpecificPackage).
-- When declared at top level (packageConfigLocalPackages), the PackageConfig must contain a program-options stanza/program-locations for these fields.
traverse_ (knownField . BS.pack . (<> "-options")) knownPrograms
traverse_ (knownField . BS.pack . (<> "-location")) knownPrograms
pure
PackageConfig
{ -- The haddock-all field provides a default value, but explicit declarations can override it
packageConfigHaddockForeignLibs = haddockAll <> packageConfigHaddockForeignLibs'
, packageConfigHaddockExecutables = haddockAll <> packageConfigHaddockExecutables'
, packageConfigHaddockTestSuites = haddockAll <> packageConfigHaddockTestSuites'
, packageConfigHaddockBenchmarks = haddockAll <> packageConfigHaddockBenchmarks'
, ..
}
where
noopLens f s = s <$ f mempty

Now everything has names. If you remove a field, you get a warning about an unused variable (and GHC will be able to pinpoint which line exactly). If you add a field, you get a warning about uninitialized field (and GHC will tell you the name of the missing field).

Template B: This PR does not modify behaviour or interface

Include the following checklist in your PR:

  • Patches conform to the coding conventions.
  • Is this a PR that fixes CI? If so, it will need to be backported to older cabal release branches (ask maintainers for directions).

@Bodigrim Bodigrim force-pushed the applicative-do branch 2 times, most recently from c58e41f to fb25a1d Compare July 9, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants