Updating values with record syntax when data constructor is not exported

I have discovered that (in a different module to the one providing the type definition) a value can be updated using record syntax even if the data constructor is not exported, as long as the field name is exported.

That came as a surprise. I can’t see that is expressly discussed in the Haskell 2010 Language Report in Sections 5.2 (Export Lists) and Section 5.8 (Abstract Datatypes) or in Section 3.15.3 (Updates Using Field Labels).

In Section 3.15.3 it seems that the ‘translation’ of the x { f1 = 1} example works, even if the translation would not be valid code because data constructors C1 and C2 are not available.

Should I have been surprised? Is this explained somewhere that is authoritative?

3 Likes

From section 5.2 (Export Lists) on page 83 of 329 in the Haskell 2010 Report:

So the data constructors which have the field label need not be exported with it.

1 Like

Translations in the Report don’t generally care about what constructors are in scope at the use site. The translation of if-then-else involves matching on True and False, but if those constructors aren’t imported then the translation still works:

import Prelude hiding (Bool(..))

main :: IO ()
main = if 0 == 0 then putStrLn "yes" else pure ()
3 Likes

I see, a reader can infer that ‘translation’ cannot mean ‘translation into alternative valid code’ because that would not be true in all circumstances given the example in Section 3.6 (and others). That is rather subtle!

1 Like

I understood from the Language Report that field names (accessors) could be exported independently from data constructors. What I did not appreciate was that they could be used to construct (updated) values, absent data constructors.

Having looked at section 3.15.3 (Updates Using Field Labels), I agree with your statement:

So the only other explanation I can think of right now is from section 5.5.3 (Closure):

1 Like

How this tripped me up is that Haddock documentation does not distinguish an exported field name of a non-exported data constructor from a regular function. There is nothing (automatic) to indicate that the function can also be used with record syntax to update. EDIT1: So authors of documentation need to add manually that the function has that property.

EDIT2: This is discussed in the context of an open issue from 2015 at Haddock’s recently archived GitHub repository, namely:

The current implementation is as you say.

The Haskell Report is silent-ish on this question, so it’s a good point. It describes many things in terms of their expansions, but carefully explains that the variables and constructors used in the expansion need not themselves be in scope: “Free variables and constructors used in these translations always refer to entities defined by the Prelude. For example, “concatMap” used in the translation of list comprehensions (Section 3.11) means the concatMap defined by the Prelude, regardless of whether or not the identifier “concatMap” is in scope where the list comprehension is used, and (if it is in scope) what it is bound to.”

To be fair, this sentence is specifically about Prelude entities, and in a record update the constructors involved are user-defined ones, which is why I say “silent-ish”. It should be specified explicitly, not left to textual analysis!

As you say the status quo is not the only possible design. An alternative design would be to require that the data constructors – or at least those that mention that particular field – are also in scope. Would it be a better design? I think one could argue either way.

You are right that it should be specified in the Report, however. I suppose that we could add something to the user manual, to clarify.

2 Likes

@simonpj, many thanks (and thanks also to other posters).

If clarifications to the Language Report are possible, I think making the design choice clear is a good candidate.

I suggest the first sentence of Section 3.15.3 could be changed from:

Values belonging to a datatype with field labels may be non-destructively updated. …

to

Values belonging to a datatype with field labels in scope may be non-destructively updated, even if the constructor is not in scope. …