Purescript: Derived Ord instances for records order labels alphabetically

Created on 26 Jan 2018  路  3Comments  路  Source: purescript/purescript

When deriving an Ord instance for a record, are the values supposed to be compared after ordering the labels alphabetically instead of using the declared order?

As an example, consider the following year/month type:

data YearMonth = YearMonth { year :: Int, month :: Int }

derive instance eqYearMonth :: Eq YearMonth
derive instance ordYearMonth :: Ord YearMonth

And a comparison between values representing December 2018 and January 2019:

> compare (YearMonth { year: 2018, month: 12 }) (YearMonth { year: 2019, month: 1 })
GT

Now, consider a month/year type:

data MonthYear = MonthYear { month :: Int, year :: Int }

derive instance eqMonthYear :: Eq MonthYear
derive instance ordMonthYear :: Ord MonthYear

This is the comparison between December 2018 and January 2019:

> compare (MonthYear { month: 12, year: 2018 }) (MonthYear { month: 1, year: 2019 })
GT

Finally, consider a non-record year/month type:

data YearMonth = YearMonth Int Int

derive instance eqYearMonth :: Eq YearMonth
derive instance ordYearMonth :: Ord YearMonth

And the comparison between December 2018 and January 2019:

> compare (YearMonth 2018 12) (YearMonth 2019 1)
LT

Are all of these the expected results? Based on the conversation in #1870, I think that the derived instances should use the declared order instead of ordering the labels alphabetically. If this is not the case, is it (or should it be) documented somewhere?

The motivation for opening this issue is that a project we (@stackbuilders) are working on is sharing a MonthYear type between Haskell and PureScript and the derived Ord instances behave differenty (the Haskell one works like the non-record type above), which is confusing. For more motivation, I think that https://github.com/purescript/purescript/pull/1870#issuecomment-186860195 is a great summary of why ordering labels alphabetically is unexpected.

Most helpful comment

I'm not sure if there was extra discussion after the discussion in #1870 that you linked to, but it would indeed seem that this behaviour doesn't match with what was discussed there.

However, there has been an important development since then; now that we have RowToList we can actually define an Ord (Record r) instance in library code, provided that each type appearing in the row r has an Ord instance. Therefore it might be reasonable to expect that a derived Ord instance for a newtype such as your YearMonth uses the underlying Ord (Record r) instance. This instance, being implemented in library code, wouldn't be able to distinguish between { year :: Int, month :: Int } and { month :: Int, year :: Int }.

In general I think we should be aiming to have as little magic inside the compiler as possible, i.e. we should eventually aim to provide a mechanism by which instance deriving strategies can be defined in libraries. Since library code can't obtain this information I wonder if it might actually be best to leave this how it is; if we switch Ord deriving for records to use the field order declared in the source file we'll struggle to 'de-magic-ify' it later.

All 3 comments

I can't answer the questions asked. You might already know this, but it's important to point out that records in PureScript are not the same as records in Haskell. That might be causing some of the confusion. Records are just different in Haskell.

In particular, records in PS are not ordered while records in Haskell are ordered. If you write data YearMonth = YearMonth { year :: Int, month :: Int }, it means something completely different in PS than it does in Haskell (even though they're syntactically similar). You're not defining a single case with two arguments that have accessor functions in PS. You're defining a single case with an anonymous record as the argument.

If you write this in Haskell:

data YearMonth = YearMonth { year :: Int, month :: Int }

A close representation in PS would be:

data YearMonth = YearMonth Int Int

year :: YearMonth -> Int
year (YearMonth y _) = y

-- Records in Haskell also create setters.
year' :: YearMonth -> Int -> Int
year' (YearMonth _ m) y = YearMonth y m

month :: YearMonth -> Int
month (YearMonth _ m) = m

month' :: YearMonth -> Int -> Int
month' (YearMonth y _) m = YearMonth y m

To emphasize the difference a bit more. Idiomatic PS would use a newtype instead of a data:

newtype YearMonth = YearMonth { year :: Int, month :: Int }

Which won't work in Haskell.

I'm not sure if there was extra discussion after the discussion in #1870 that you linked to, but it would indeed seem that this behaviour doesn't match with what was discussed there.

However, there has been an important development since then; now that we have RowToList we can actually define an Ord (Record r) instance in library code, provided that each type appearing in the row r has an Ord instance. Therefore it might be reasonable to expect that a derived Ord instance for a newtype such as your YearMonth uses the underlying Ord (Record r) instance. This instance, being implemented in library code, wouldn't be able to distinguish between { year :: Int, month :: Int } and { month :: Int, year :: Int }.

In general I think we should be aiming to have as little magic inside the compiler as possible, i.e. we should eventually aim to provide a mechanism by which instance deriving strategies can be defined in libraries. Since library code can't obtain this information I wonder if it might actually be best to leave this how it is; if we switch Ord deriving for records to use the field order declared in the source file we'll struggle to 'de-magic-ify' it later.

Closing this since it's intentional, and I think preserving the same ordering between deriving and RowToList is desirable.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

srghma picture srghma  路  3Comments

klntsky picture klntsky  路  3Comments

LiamGoodacre picture LiamGoodacre  路  3Comments

hdgarrood picture hdgarrood  路  4Comments

garyb picture garyb  路  3Comments