Skip to content

Add HasField instances for tuples to allow tuple-indexing #143

@BinderDavid

Description

@BinderDavid

TL;DR

Add instances of the following kind to base and make them available via the Prelude:

instance HasField "_1" (a1,a2,a3) a1 where
  getField (x,_,_) = x

instance HasField "_2" (a1,a2,a3) a2 where
  getField (_,x,_) = x

instance HasField "_3" (a1,a2,a3) a3 where
  getField (_,_,x) = x

In order to allow access to tuple elements using record dot syntax like this:

(true, "hello", 42)._1 == true
(true, "hello", 42)._2 == "hello"
(true, "hello", 42)._3 == 42

Motivation

Accessing elements of tuples other than 2-tuples is currently cumbersome, since the Prelude defines fst and snd functions only for 2-tuples. We have to pattern match on n-tuples explicitly in order to access the elements of the tuple. Other languages provide some form of tuple-indexing, which makes these n-tuples much more convenient. With the OverloadedRecordDot we can use this newly available mechanism to enable convenient tuple indexing as well.

What about another name for the field?

In an ideal world we could use the syntax (true, "hello", 42).2 without the underscore. This is, for example, the syntax that Rust uses for tuple-indexing: Rust by Example. As far as I can tell, this doesn't currently work since this expression cannot be parsed. If we want this syntax instead, then the parser/lexer would have to be changed, and this couldn't be a CLC proposal but would have to be turned into a GHC proposal.

What about Lens / Optics

Both the Control.Lens.Tuple and the Data.Tuple.Optics modules provide accessors using the same naming scheme.
With both Lens and optics you can use the syntax (1,2) ^. _1.

I think using the Lens/Optics libraries is not a proper solution to the basic usability problem of tuples outlined above.
I just want to make it easier to access fields in a tuple, not use a big library to permit abstraction over access into nested data structures which requires additional imports and dependencies. If these instances are exported in the Prelude, then I never have to add any imports, and can just use the record dot syntax (if I have OverloadedRecordDot enabled). Record dot syntax is also much more newcomer friendly. If we do want to use the full expressive power of Lens/Optics, then this proposal actually provides an easier onboarding ramp, since the Lens and Optics libraries use the same names for tuple fields. The HasField instances and the Lens/Optics accessors can also peacefully coexist in the same codebase.

Can I try it out?

Yes, you can add a dependency of my prototype implementation tuple-fields on Github

What about Solo?

For consistency reasons, Solo should also get its instance:

instance HasField "_1" (Solo a) a where
  getField (Solo x) = x

Should instances for all n-tuples be provided?

Currently the largest tuples are 62 tuples. In https://github.com/BinderDavid/tuple-fields/blob/main/src/Data/Tuple/Fields.hs I added all instances for all tuples. The Lens and Optics libraries don't support field access for all these tuples. I guess that the reason might be due to excessive compilation time? Adding all instances would be the more consistent choice, but I don't know the details of how that interacts with the complexity of instance search.

What about unboxed tuples?

Adding the corresponding instances for unboxed tuples is currently not possible.
The following instance:

instance HasField "_1" (# a1,a2,a3 #) a1 where
  getField (# x,_,_ #) = x

currently results in the following error:

Main.hs:17:24: error: [GHC-83865]
     Couldn't match a lifted type with an unlifted type
      Expected kind ‘*’,
        but (# a1, a2, a3 #) has kind TYPE
                                           (TupleRep [LiftedRep, LiftedRep, LiftedRep])
     In the second argument of HasField, namely (# a1, a2, a3 #)
      In the instance declaration for HasField "_1" (# a1, a2, a3 #) a1
   |
17 | instance HasField "_1" (# a1,a2,a3 #) a1 where
   |                        ^^^^^^^^^^^^^^

This is because the HasField typeclass is currently not representation-polymorphic enough.
Discussions about making the typeclass more polymorphic are discussed here https://gitlab.haskell.org/ghc/ghc/-/issues/22156 and here https://github.com/adamgundry/ghc-proposals/blob/hasfield-redesign/proposals/0000-hasfield-redesign.rst#recap-planned-changes-to-hasfield Since adding these instances is currently not possible, adding them is out of scope for this proposal.

Backwards Incompatibility

I don't currently see how this could break backwards compatibility in any way. Afaik, as soon as the OverloadedRecordDot extension is enabled, the lexing rules around the .symbol change so that foo.bar without whitespaces can only be used for field access. So there should be no conflicts with existing uses of the tuple accessors from the Lens or Optics libraries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dormantHibernated by proposer or committee

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions