How to properly use Record types?

I probably have all the Record extensions enabled at this point, and was hoping for some smooth sailing if using Records extensively, yet I have hit some headscratchers.

To ilustrate, I have the following definitions

makeRequest :: String -> Request
makeRequest url = Request { method = "GET", url, headers = [] }

data Request = Request {
  method :: String,
  url :: String,
  headers :: [(String,String)]
} deriving stock (Show)

data Response a = Response {
  status :: {-# UNPACK #-}!(Int, ByteString),
  headers :: Network.HTTP.Types.ResponseHeaders,
  body :: a
}

And when I load this up into GHCi, and play around I get into the following two scenarios.

> r = makeRequest "https://example.com"
> r { headers = ("lorem","ipsum") : r.headers }

<interactive>:3:1: error:
    ā€¢ Record update is ambiguous, and requires a type signature
    ā€¢ In the expression: r {headers = ("lorem", "ipsum") : r.headers}
      In an equation for ā€˜itā€™:
          it = r {headers = ("lorem", "ipsum") : r.headers}

> r { headers = ("lorem","ipsum") : r.headers } :: Request

<interactive>:4:5: warning: [-Wambiguous-fields]
    The record update r {headers = ("lorem", "ipsum")
                                     : r.headers} with type Request is ambiguous.
    This will not be supported by -XDuplicateRecordFields in future releases of GHC.

<interactive>:4:5: warning: [-Wambiguous-fields]
    The record update r {headers = ("lorem", "ipsum")
                                     : r.headers} with type Request is ambiguous.
    This will not be supported by -XDuplicateRecordFields in future releases of GHC.
Request {method = "GET", url = "https://example.com", headers = [("lorem","ipsum")]}

I know that Response a and Request data types share the same record field headers, but how come GHCi doesnā€™t ā€œseeā€ the context in which header is used given that r is a concrete type?

Second, given the deprecation warning, how should I approach use-cases like mine going forward? I like the ergonomics of the sample code Iā€™ve written, but seems like itā€™s not in line with the direction Haskell is taking.

3 Likes

This GHC proposal has a bit of background on the status quo and planned changes for updates of duplicate record fields: ghc-proposals/0366-no-ambiguous-field-access.rst at master Ā· ghc-proposals/ghc-proposals Ā· GitHub

In the future, the forthcoming OverloadedRecordUpdate extension should provide one way for updates like in your examples to be accepted without an issue. However, it is unfortunately not ready for general use yet.

In the meantime, there are a few options:

  • Avoid using the same field name multiple times (e.g. with type-specific prefixes/suffixes)
  • Disambiguate using the module system (e.g. qualified imports)
  • Various lens/optics libraries can be used for polymorphic updates (e.g. see Optics.Label for the approach recommended by the optics package)
  • Use record-dot-preprocessor to get early access to something approximating the future GHC feature

Disambiguate using the module system (e.g. qualified imports)

This post explains the qualified imports trick.

It would be something like

    import qualified Main as Request ( Request (..) )
    import qualified Main as Response ( Response (..) )
    r { Response.headers = "lorem" : r.headers }

By importing the module qualified under different names, and only importing one of the records each time, the qualified field references cease to be ambiguous.

Alas, it doesnā€™t work in the same module which defines the records, because a module canā€™t import itself.

4 Likes

OverloadedRecordUpdate looks exactly like what I need. Even if itā€™s marked experimental, GHC moves at a faster pace than I do!, so Iā€™ll probably be fine.

Somehow I have overlooked this extension when Iā€™ve enabled OverloadedRecordDot.

WIth OverloadedRecordUpdate + RebindableSyntax (which is a requirement of the former), because Iā€™m operating with NoImplicitPrelude Iā€™m currently stuck on Not in scope: ā€˜ifThenElseā€™. I canā€™t find via Hoogle any reference in base for ifThenElse that I can import.

Is the if/then/else construct not exported from base as a function as well, and am I always required to define my own when RebindableSyntax + NoImplicitPrelude are enabled?


This could spin off into itā€™s own discussion. But what about updating the GHC2021 language proposal? Iā€™ve used that as a starting point for all the extensions I want to have/enable in my project, and given itā€™s original date itā€™s missing a few extensions from the list (like the OverloadedRecordā€¦ ones).

Would it be sensible to add a horizontal market to that document and mention the additional extensions that have been released since, for completionā€™s sake? Or are proposals immutable documents once approved?

The requirement for RebindableSyntax is what I mean by OverloadedRecordDot not being ready for general use yet. You need to supply your own definition of a setField function to use when desugaring record updates, and generally speaking RebindableSyntax is a bit of a heavy hammer which you probably donā€™t want to use unless you really need it. (That said, the specific issue about ifThenElse is discussed at base: export default ifThenElse implementation for RebindableSyntax (#18081) Ā· Issues Ā· Glasgow Haskell Compiler / GHC Ā· GitLab).

Re GHC2021, I think the plan is that there will at some stage be another GHC202x which makes changes to the extensions included in the language version. I donā€™t know if there is a concrete plan for when that will be, and it depends on the proposal discussion and GHC Steering Committee decisions as to which extensions are included.

The process for GHC20XX language versions is detailed here: ghc-proposals/0372-ghc-extensions.rst at master Ā· ghc-proposals/ghc-proposals Ā· GitHub

Extensions must be widespread and stable (among other factors) before they are accepted into a new GHC20XX language version. So, I think these overloaded record extensions will not be added any time soon.

Thanks for the reference links to the issue I had around the ifThenElse, and others I havenā€™t encountered yet like the setField instances. I might hold of on this approach and bear with the additional type annotations and deprecation warnings.

In terms of GHC20XX didnā€™t intend to imply that those extensions should be part of it, just if the proposal could be extended to include references to additional extensions that GHC supports, which werenā€™t considered for GHC2021. When I initially landed on the GHC2021 proposal page I implicitly expected that all extensions are listed on that page, unaware that there were newer extensions that arenā€™t even mentioned.

TL;DR when I get around to it might send a PR to the GHC2021 proposal document to add a footer section that acknowledges that the list of extensions mentioned is not comprehensive, and reference new extensions that surfaced since.