Why NoFieldSelectors + DuplicateRecordFields disallows different types

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoFieldSelectors #-}

data Error
  = FooError { x :: Int }
  | BarError { x :: Bool }

These two constructors would be valid in separate data types with the extensions, but not if they’re different constructors in the same data type. Presumably it’s because \(e :: Error) -> e{x = 1} is ambiguous? Wouldn’t it be possible to relax this restriction and only error at the ambiguous call-site instead of at the definition site?

1 Like

Yes, this restriction should be able to be lifted. I complained about it here, as that thread turned in to a pretty broad discussion about fields in sum types.

These two constructors would be valid in separate data types with the extensions, but not if they’re different constructors in the same data type.

That does seme a bit odd, I agree.

Presumably it’s because \(e :: Error) -> e{x = 1} is ambiguous?

Actually record update is handled by expansion so we’d get

\e -> case e of
          FooError {} -> FooError { x=1 }
           BarError {} -> BarError { x=True }

and that wil simply produce a type error, resonably enough.

Worth some discussion, some implementation investigation, and a small GHC proposal perhaps. Since this will only allow more programs, not fewer, it should not be controversial.

1 Like

This would still break with OverloadedRecordDot, which needs to be able to generate an unambiguous HasField "x" Error ??? instance. (and it needs to do that depending on whether it is enabled at the point of usage, not of definition)

In theory it would be possible to relax this restriction while also not generating HasField instances for types with selectors of incompatible types though.

(and if we’re doing that already maybe we can also break backwards compatibility a little and remove partial HasField instances while we’re at it)

Ah makes sense, thanks. Another interesting approach with https://github.com/ghc-proposals/ghc-proposals/pull/535, is to generate a HasField with Eithers (or maybe unboxed sums?). But I agree, I think it would be nice to lift the restriction and simply avoid generating HasField instances for fields with different types

Excellent point!

In theory it would be possible to relax this restriction while also not generating HasField instances for types with selectors of incompatible types though.

Hmm. I think we already don’t generate HasField instances for fields with existential type, e.g.

data T where
  MkT :: { x :: a, f :: a->Int } -> T

This is very sadly un-undocumented (so far as I can see) in the user manual. But in GHC today, even without OverloadedRecordDot we won’t generate field selector for the so-called “naughty” fields x and f. This extends to OverloadedRecordDot. And it’s not just existential fields, also higher rank ones.

Here’s a comment fragment from GHC.Tc.Errors:

    HF2c. The record field type 'fld_ty' contains existentials variables
          or foralls. In the former case GHC doesn't generate a field selector
          at all (it's a naughty record selector), while in the latter GHC
          doesn't solve the constraint, because class instance arguments
          can't contain foralls.

So if would be perfectly resonable to add an extra condition that, with NoRecordSelectors and OverloadedRecordDot we don’t get field selectors for fields that have multiple types in different contructors. Not a new thing, just extending the scope of an existing thing.

The existing behaviour should be documented much more carefully too!

4 Likes

This is documented in section 6.5.9.1 Solving HasField constraints (see the bullet points after “Solving HasField constraints depends on the field selector functions that are generated for each datatype definition:”). Is there something missing from there?

FWIW, I agree that it would seem reasonable to permit records to have fields of different types, and merely omit generating a selector function in such cases, just as we do for existentials. That doesn’t even particularly need to be tied to NoFieldSelectors; after all, it isn’t for existentials. Anyone feel up for writing a GHC proposal?

I’ll try to get around to a proposal. It seems like basically lift the restriction globally and treat records with different types the same as existential types? So no HasField, no x{a = a}, etc? Is there something existential fields can do that you couldnt do with records of different types, or vice versa?

I agree with Adam this behaviour was always clear back to the HasField design work. (Whether the documentation was discoverable in the ‘right’ place is more of a general difficulty.)

I don’t think we can expect NoFieldSelectors to behave differently to the underlying H98 data decl behaviour. Note the data decl might get imported into a different module using ordinary H98 field access.

Same field label at different field type has been available in a Haskell since 1996: Hugs/Trex. I’ve been applying some tweaks to it to work more smoothly with Overlapping Instances and FunDeps — see the Hugs-Users mailing list.

Existentials allow for record updates:

ghci> data T = forall a . MkT { foo :: a }
ghci> :t \x -> x { foo = 3 }
\x -> x { foo = 3 } :: T -> T
ghci> foo (T 3)

<interactive>:12:1: error: [GHC-55876]
    • Cannot use record selector ‘foo’ as a function due to escaped type variables

whereas if there are fields of different types, I think we’d want to forbid both selection and update. (I suppose in principle one could allow an update provided the new value had an appropriate type for both constructors, but allowing that seems like it would introduce too much complexity into the type system.)

1 Like

The documentation there says “If a record field does not have a selector function because its type would allow an existential variable to escape,…” But where do we document which record fields have selectors, and what the rules are? And with NoRecordSelectors no record field has a selector function, yet we still (of course) solve HasField constraints.

And is this statement an “iff”? That is: if the record has a selector, then HasField will work for it, and vice versa? No: higher rank messes that up, as the next bullet says.

The user manual makes true statements but they feel incomplete. Documentation is hard!

2 Likes

Wrote up a quick proposal: Differently typed duplicate record fields by brandonchinn178 · Pull Request #733 · ghc-proposals/ghc-proposals · GitHub

3 Likes

I think that’s a poor guiding principle for programming language syntax. As if we should go around looking for programs that are currently rejected and dream up some semantics to make them valid. There’s already a danger with all those GHC extensions that a newbie might make a mere typo and get the compiler suggesting ‘Did you mean to enable advanced X?’

A better guiding principle is for any extension to entail syntax with clear blue water separating it from common-styled code. ScopedTypeVariables is a particularly egregious example.

it should not be controversial.

I find the proposal as written-up controversial. I’ve put feedback there. In particular that there’s no formal extension/pragma to verify the programmer’s intent in writing a non-H98 compliant data decl.

I’ll respond to your latest comment when I’m back at my laptop, but I want to respond to this:

The extension is DuplicateRecordFields. Maybe I didn’t emphasize it enough in the proposal, but the proposal isn’t changing anything when DuplicateRecordField is disabled