At this rate, I wonder if any extensions are suitable for GHC2024. If we get it at all, I imagine we’ll have to make compromises somewhere along the line — some people will be unhappy, but that’s how it is.
Yes, I just think it is not a good idea to add even more ways to trigger that unsafety to the default language. As I said:
I wouldn’t want to encourage everyone to start using coerce everywhere and so I don’t want to encourage everyone to use DerivingVia everywhere either.
But I think there is a relatively easy fix: declare once and for all that Data.Coerce is safe (and perhaps check Hackage for unsafe existing code). If we can collectively make that decision (and we improve the error messages) then I think it is fine to add DerivingVia.
The desire to consider Data.Coerce to be Safe is also the motivation for including RoleAnnotations in GHC2024, to indicate that these days, if you want to define an abstract data type, you really have to think about roles as well.
My vote, unavailable in the poll, is this: require GHC2024, whatever it implies, to be specified explicitly. If there’s no {-# LANGUAGE GHCxxxx #-} pragma then the default should be {-# LANGUAGE Haskell2010 #-}.
I, for one, am happy GHC2021 exists so we can use it in our production codebase. Adding a few more syntactic sugar pragmas and other pragmas with explicit “opt-in” syntax would also be appreciated
(e.g. DerivingStrategies, that you don’t use unless you add keywords in specific places)
I’m aware of that. I also think it was a mistake. It may be too late to fix the mistake now, but if not, the time to do it is when the new default is set.
Back on topic: Haskell is becoming and will become a relatively large language. It is convenient and most likely the right path, but I am happy to have hopped in when everything was easier to grasp, because there were less things to learn, and now and then a blog-post popped up with a new extension tutorial.
I mentioned that normally you’d have a dangling do (i.e. putting a newline after the do), but used a run-on line to exaggerate my point.
funcName =
-- otherFunc :: a -> m b -> _
otherFunc arg1 do
someOther arg2
moreThings toDo
This, to me at a cursory glance, looks the same as the following:
funcName =
-- otherFunc :: a -> b -> c -> d -> e -> f -> _
otherFunc arg1 arg2
someOther args
moreThings toDo
So I’d just like a non-alphanumeric separator showing that it’s not just a bunch of arguments, so either the $ operator, or (...) parentheses to enclose the part. But I understand some people don’t have this issue, which is fine, but I just don’t like BlockArguments
P.S. And an example from this section from Typeclasses.com shows something I also dislike because it takes too long (for me) to understand the parsing order when seeing this at a glance:
{-# LANGUAGE BlockArguments #-}
import Control.Concurrent.Async (concurrently_)
blockStack =
concurrently_
do
putStrLn ['a'..'z']
do
putStrLn ['A'..'Z']
Honestly I wish RecordWildCards were banned and completely removed from GHC, RecordDotSyntax is strictly better in every aspects. I can understand why people felt the need for it at times but this should just be history now, it’s a terrible terrible extension.
Is ‘should now be history’ premature? RecordWildCards has been supported from GHC 6.8.1 but OverloadedRecordDot and OverloadedRecordUpdate are only supported from GHC 9.2.1 and the latter comes with an ‘EXPERIMENTAL’ warning in the documentation for GHC 9.2.1 to GHC 9.8.1.
Am I right to understand that, to get its full syntax benefits, moving from RecordWildCards to record dot syntax involves, in practice, the renaming of fields to remove the prefixes that identify the type? Taking my Stack code base example:
Step 1: just use a dot syntax:
-- | Interprets BuildOptsMonoid options.
buildOptsFromMonoid :: BuildOptsMonoid -> BuildOpts
buildOptsFromMonoid buildMonoid = BuildOpts
{ boptsLibProfile = fromFirstFalse
(buildMonoid.buildMonoidLibProfile <>
FirstFalse (if tracing || profiling then Just True else Nothing))
...
-- 26 other fields relying on the 31 fields of a `BuildOptsMonoid` value
...
, boptsDdumpDir = getFirst buildMonoid.buildMonoidDdumpDir
}
where
...
Step 2: also eliminate the use of prefixes in field names to identify the type:
-- | Interprets BuildOptsMonoid options.
buildOptsFromMonoid :: BuildOptsMonoid -> BuildOpts
buildOptsFromMonoid buildMonoid = BuildOpts
{ libProfile = fromFirstFalse
(buildMonoid.libProfile <>
FirstFalse (if tracing || profiling then Just True else Nothing))
...
-- 26 other fields relying on the 31 fields of a `BuildOptsMonoid` value
...
, ddumpDir = getFirst buildMonoid.ddumpDir
}
where
...
Currently, there are 118 instances of {..} in the Stack project: 39 in the pantry library and 79 in Stack itself. I am happy to work through eliminating all of them but I read the guidance in the GHC documention as saying, in terms, ‘hold off from doing that - things may change’.
I disagree. For one, RecordDotSyntax is more magical. I had a record field with an existential forall, and I don’t think it worked with RecordDotSyntax. Plus, I view the whole syntax as experimental, since RecordDotUpdate doesnt work out of the box.
RecordWildCards is sometimes nice for initializing all fields, e.g. with QuickCheck:
a <- arbitrary
b <- arbitrary
pure X{..}
I’m pretty sure -Wall protects against missing a field, and this is protected against reordering fields, unlike the X <$> ... <*> ... syntax. RWCs isn’t too confusing for such a small function body.
Regardless how you feel about RWCs, it’s incorrect to say RecordDotSyntax is strictly better, there are definitely things RWCs can do that DotSyntax cant
Fine, it’s not strictly better in every aspect, it’s largely better in significant aspects on every use-case where they overlap. Now try to use (let alone refactor) any code-base where RecordWildCards is used and feel the pain. Typing 20 fields and using the type constructor is simply order of magnitudes more simple and readable in the definition of a record, using implicitly all the names that match in the context is a recipe for disaster. For instance if you add a field in the record that happens to already be in the context of some use-cases it’s simply impossible to detect, except for carefully examining every place you used it. Now of course I’m not saying this is likely to happen frequently, but just one time is too often.
All in all I’m not going to argue over this, I just experienced it first hand and I think advocating for using it is mostly a bad trade favoring initial code write over code maintenance. Which I think is a deep mistake but that’s just me.
Regarding the refactoring @mpilgrem, I think moving over to NamedFieldPuns is the correct thing to do for now. This is again, strictly better in every aspect where they overlap, except for a very small burden initially.
You’ll have to pardon my ignorance on this matter - any mention of records in Haskell these days beyond a trivial remark usually results in me just ignoring it out of annoyance - but if you’re going to repeatedly write out all those fields and the associated constructor at each site of use…why bother with any form, style or variety of record notation at all? Just use regular pattern matching.