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.

5 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.

7 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.

1 Like

Thanks for the link and summary. I stumble upon this issue from time to time.

As i understand the OverloadedRecordUpdate will be the final solution. Is there any roadmap/progress on that extension? Record updates seem like such a basic feature that should just work, and might trip up and discourage Haskell beginners.

See How do you avoid the -Wunused-top-binds with makeLenses - #4 by arybczak.

Nothing has changed since I wrote this post.

2 Likes