Allow defining named records

I mostly agree with this. My consolation is that this doesn’t change syntax or semantics of the language, so it doesn’t affect tooling (at least any that exist). It’s just inserting code that you would otherwise write yourself, so I think it’s more along the lines of instance deriving.

You only need to wrap/unwrap because you can’t use record syntax with them. If you had overloaded record syntax you wouldn’t need to manually wrap or unwrap at all.

@natefaubion
Suppose we have the following:

newtype Person = Person { name :: String, address :: String }

instance eqPerson :: Eq Person where
  eq (Person person1) (Person person2) = 
    person1.name == person2.name

comparePersons :: Person -> Person -> Boolean
comparePersons person1 person2 = person1 == person2

comparePersons' :: Person -> Person -> Boolean
comparePersons' (Person person1) (Person person2) = person1 == person2

While comparePersons uses Eq instance Person's Eq instance, comparePersons' uses Record's Eq instance that has a different behaviour.
If I found function comparePersons' in a code base, I would be wondering whether the use of Record's Eq instance was intentional or an error.
I think that this is increase in accidental complexity is related to the fact that two types are used instead of one: the record itself: { name :: String, address :: String } and the wrapper: Person.
With named records, we would only have one type (similar to Haskell), and this would not be a problem. That’s why I think that named records, in addition to overloaded syntax for records, would be ideal.
What do you think?

That’s a feature as far as I’m concerned - one of the motivations for newtype is using them to direct a different instance choice than the default. Types often have multiple sensible instances, depending on use case.

1 Like

@garyb
I agree that it’s normal to use newtypes to define different instances. However, I think that in some situations it may be a bit problematic to have Eq, Ord and Show instances derived automatically for records, as such instances will not always be sensible. This also means that in many cases we will have two instances for Eq, Ord and Show, when only one is really needed.
I think that writing or deriving instances should be up to the programmer.
With named records, we can have only one type, and instances for Eq, Ord, and Show may be “opt-in”: one may choose to derive them and get a default record instance, write a custom instance or do neither of those, similar to Haskell.

I think the reason this discussion isn’t really going anywhere, is that, for me at least, I don’t really know what is trying to be solved here - as far as I’m aware newtype already handles all of the problems listed aside from syntax ergonomics.

The most commonly raised issue that people have with the syntax aspect is the inability to access fields without unwrapping the newtype. I certainly understand that, as writing getters or having to unwrap can be quite tedious.

I think the case for needing overloaded record literals in general is much less common, but there are times where it would be nice.

So hypothetically, if those things were possible (dot access, literals), what other problems would this proposal be solving? (Aside from the comparePerson one listed above. Sorry - I’m going to reject that one - the arguments you most recently gave against it apply equally to all newtypes, and they aren’t going away any time soon! The inner record instances are not automatically derived, they’re just the instances for Record row, similar to if it was a newtype over some Maybe it’d be the instances for Maybe a).

1 Like

I’m going to echo what @garyb said. I’m not sure it’s accidental complexity, but more separation of orthogonal features. This is how PureScript has always worked with structural types, and I would consider this issue to be uncommon enough that it’s difficult to justify an entirely new feature around it. Just because something can happen doesn’t mean we need a language feature to prevent it. We need evidence that it’s a problem causing people pain. One person’s accidental complexity is another’s feature. Other issues:

  • You still have to have eliminators for these types, and you have to do that in a way that doesn’t conflict with records. Overloaded syntax doesn’t solve this because you still need a primitive to desugar into.
  • Rows and records are unordered, but I don’t think that’s going to be the expectation for product types like this.
  • What about sum types? There is a single way to do things right now, and I’m not inclined to give up the uniformity.

This isn’t a lightweight feature once you consider how it has to interact with all the other features of the language. There are potentially answers to all these questions, but I just don’t think it’s worth it. With overloaded syntax, there are several avenues to derive instances for our standard data types however.

3 Likes

@garyb
Of course, there is nothing wrong with newtypes in general. What I think can be improved is the need to newtype every time we need to create an instance for a record. You wrote earlier yourself “Besides, there are many problems with overlapping instances and such like if we allowed people to define them. The workaround for now is to newtype.”


We can easily define instances for sum types and any other types in Purescript directly, but not for records (while other languages like Haskell, Frege, Idris allow this). I understand that this is because records are structurally typed, and that’s why I am proposing to add named records that have a nominal component to their typing.

Today if we have:
type Person r = Record ( name :: String | r )
We cannot define instances for this directly, we need to newtype.

If we allow something like:
newrecord Person r = Person ( name :: String | r )
Then we can allow instances for Person, and there will be no overlapping, as Person (name :: String | r) is not the same as Company ( name :: String | r ).

Then, we don’t need newtype wrappers. Also, with newtype wrappers we have two types, but with named records we only have one type, and I think that this can make it much easier to work with.

I guess it just comes down to me not feeling the pain as strongly as you, as aside perhaps from wanting overloaded dot access to avoid having to write getters, this just doesn’t come up as something that bothers me in my day-to-day PureScripting.

The desire to write instances for records is pretty rare for me - and thinking about it, in the cases it crops up, I usually don’t want to allow access to the record internals anyway (to preserve invariants), so end up exporting the newtype for it as an opaque type.

Fwiw, aside from syntax overloading, the named record thing is already possible with existing features:

newtype NamedRecord (sym :: Symbol) r = NamedRecord (Record r)

instance eqA :: (RowToList r a, EqRecord a r) => Eq (NamedRecord "person" r) where
  eq (NamedRecord a) (NamedRecord b) = eq a b

instance eqB :: (RowToList r a, EqRecord a r) => Eq (NamedRecord "company" r) where
  eq (NamedRecord a) (NamedRecord b) = eq a b

eqPeople :: NamedRecord "person" (name :: String) -> NamedRecord "person" (name :: String) -> Boolean
eqPeople = eq

eqCompany :: NamedRecord "company" (name :: String) -> NamedRecord "company" (name :: String) -> Boolean
eqCompany = eq

failEq :: NamedRecord "person" (name :: String) -> NamedRecord "company" (name :: String) -> Boolean
failEq = eq

edit: I realise this isn’t exactly the same as what you were proposing, since it’s reusing basic Eq instances rather than just comparing on name, but you get the idea.

@natefaubion
I completely agree with you that we need to consider whether the feature is worth it given the type system changes and the implementation costs. For now I have mostly been focusing on why I would like to have it, so we could try to agree on whether it gives us substantial benefits.

Regarding your other points, I thought that the semantics of named records would be exactly the same as the semantics of Record. So, if we define
newrecord Person r = Person ( name :: String | r ),
Person behaves just like Record, taking a row as a parameter. So, such named records are structurally typed for each named record, but of course, Person (name :: String | r) is different from Company (name :: String | r). They are also unordered, just like normal Purescript records.

I am not quite sure what you mean when you mention sum types. I didn’t think sum types should be affected in any way. I thought that there would be even more uniformity with sum types, because currently you can define instances for sum types, but not for records.

I agree with you that having overloaded syntax for dot syntax similar to the latest proposal for records in Haskell [proposal](https://github.com/ghc-proposals/ghc-proposals/pull/282 is a good thing to have, independently of this feature, and, as @jy14898 pointed out, it can also be used to implement this feature.

I think that the till the point you describe your proposal in your last post, there is no problem adding it to the language. However, the real problems come at the point of type inference and literal overloading.

PureScript chooses to not overload basic literals. I love this. In my opinion, this makes the base language way easier to understand for beginners. The 1 :: Num a => a in Haskell is unnecessarily complicated. In PureScript:

a = 1.0 :: Float
b = [1,2,3] :: Array Int
c = {field: “Hello”} :: {field :: String} 
d = _.field :: {field :: a | r} -> a

What would be the types of c and d after implementing your language proposal? We’d need more magic classes, complicate type inference, and explain it to beginners. Because of the rule literals are not overloaded, your proposal is a no-go for me I’m afraid.

@garyb
Thanks for sharing the example, it is interesting.
However, NamedRecord in your example is also a wrapper around Record, right?
So, in a way it is similar to a normal newtype wrapper.

I would like to have a possibility to create types that have a name and have semantics of Record.
I perfectly understand that not everyone would feel the need to have something like this. Though, I think that Purescript would benefit from this. Also, I think it would be interesting to test out the idea of mixing nominal and structural typing in one record type.

@timjs

I think it may be helpful to separate literal overloading (your example “c”) and type inference in general (your example “d”).

As previously mentioned, if literal overloading is undesirable, we could specify the record type explicitly as we would do in languages with nominal record types (for example, Haskell, Frege, Idris). As Person {name : "John"} means creating a newtype wrapper in Purescript, we could use some other syntax, for example, Person : {name : "John}.

When it comes to type inference, I think that this issue is not unique to this proposal, and we would also need to deal with it if we wanted to implement overloaded dot syntax for newtype wrappers as described by @natefaubion and @jy14898 (as I previously mentioned, such overloaded syntax may probably be seen as a prerequisite for this proposal).

I think there are two possible options:

  1. Overloaded dot syntax similar to the latest https://github.com/ghc-proposals/ghc-proposals/pull/282 proposal for records in Haskell. In this case the type of “d” will be based on type classes. I think @jy14898 preferred this approach.

  2. Similar to Frege, where more context is needed to fully determine the type of “d”.

Yeah, it is still just a newtype. I’m just trying to suggest options that seem to work for what I think are your two main concerns (no unwrapping, named product type with named fields) without the need to introduce a new fundamental thing in the type system with all the knock-on effects and thinking that would require.

If I summarise this correctly, the main concern, also in the Reddit discussion posted earlier, is that the nice and convenient dot-notation cannot be used.

So what if we’d allow the sugar record.Constructor.field on term level, as in this example:

newtype Person = Person { name :: String, ... }

-- old way
show :: Person -> String
show (p@(Person p')) = otherFunc p <> p'.name 

-- new way
show :: Person -> String
show p = otherFunc p <> p.Person.name
-- which gets desugared into
show p = otherFunc p <> (case p of Person x -> x.name)  -- where x is fresh and doesn't shadow other names

This is the way my colleagues solved the problem in the Clean language (see the grammar at the end of section 5.2.1).

Benefits:

  • Use nominally typed records:
    • Can have instance declarations.
    • Cannot be mixed up with other nominally typed records.
  • Use convenient dot-syntax.
  • No more p@(Person p') as parameter any more.
  • No impact on type inference.
  • Based on existing language constructs.
  • Doesn’t clash with other qualified syntax constructs (module imports), because of lowercase.Uppercase syntax.

Drawbacks:

  • More sugar…
1 Like

Similarly to the answer on reddit, one can also do

_Person (Person p) f = f p

show :: Person -> String
show p = otherFunc p <> p`_Person`_.name

which if you squint, is fairly close to your solution

I think what would be cool is if we had a userland way of making .-like operator more directly:

getPerson (Person p) s = get s p

-- TODO: better name
symbol_infixl [somefixity] getPerson as .#

p = Person { name: "Joe" }

-- name is automatically turned into an SProxy
v = p.#name

EDIT: no doubt it’s not trivial as for some reason I seem to think . has magic to parse the following term as a symbol?

EDIT2: If you really wanted to pretend p had fields…

newtype Person = Person { name :: String }

derive instance newtypePerson :: Newtype Person _

otherFunc :: Person -> String
otherFunc (Person { name }) = "[Person named " <> name <> "]"

class AutoNewtype t o where
    autoUnwrap :: t -> o
    autoWrap   :: o -> t

instance autoNewtypeIdentity :: AutoNewtype t t where
    autoUnwrap = identity
    autoWrap   = identity

else instance autoNewtypeNewtype :: 
    ( Newtype t a
    ) => AutoNewtype t a where
    autoUnwrap = unwrap
    autoWrap   = wrap

show :: Person -> String
show p' = otherFunc p <> p.name
    where
        p = autoUnwrap p' :: forall a. AutoNewtype Person a => a
1 Like

You still need to box it somehow (at least syntactically), but that’s pretty groovy! :smiley:

Took a bit to work out the types, but this is a little nicer:

pretend :: forall t r. ((forall o. AutoNewtype t o => o) -> r) -> t -> r
pretend f v = f (autoUnwrap v)

show :: Person -> String
show = pretend $ \p -> otherFunc p <> p.name

Can you do something like:

type Auto t = forall o. AutoNewtype t o => o

show :: Auto Person -> String
show p = otherFunc p <> p.name

Yeah that works, just you have to make sure to pass the person through autoUnwrap first of course

Although interestingly it has problems with $ doing it this way, whereas pretend is fine with $. I know other rank-2 type stuff like ST has the same issue, really I’m surprised it’s not an issue with pretend (beyond my understanding…)

ie:

show $ autoUnwrap $ Person { name : "Joe" }

crashes with an eskaped skolem

(also purely by coincidence I came up with the exact same type trying to simplify reasoning about my types, exact as in character for character :open_mouth:)