I’ve recently updated a large codebase from 8.10.7 to 9.2.5, enabling -XNoFieldSelectors and -XOverloadedRecordDot everywhere, rewriting selectors to dot syntax where necessary.
During the process I had one observation which I think is worth some discussion. What I observed is that from the semantics / operational point of view, newtype accessors are not really fields. Consider the following types
newtype Key a = Key { unKey :: a }
newtype MyMonad r a = MyMonad { runMyMonad :: r -> IO a }
I must say that something was not feeling right when I had to type key.unKey or m.runMyMonad env. It feels much more natural to unKey key, or runMyMonad m env. Usually when I create a newtype, I think in terms of “actions” like “unwrap the type” or “run the monad”, not necessarily “get the unKey field” or “access the run function of this monad”. A few times I explicitly turned field selectors on on a module that declares a newtype, to retain the previous behaviour.
Overall, it summarizes to the following opinion: newtypes are not really records, and maybe their unwrap functions should not be treated as selectors (in light of -X(No)FieldSelectors extension). What are your opinions on this topic?
I agree that newtypes with fields are a bit strange. The constructor and field selector are an isomorphism, so each determines the other.
Not sure what others do, but personally I wouldn’t usually use a newtype field in a pattern match (e.g. f (Key{unKey = x} = ...) and there is little point in using them in record updates because you are replacing the whole value anyway (i.e. r { unKey = x } is the same as Key x). Thus perhaps the thing to do is to define a non-field newtype and its selector explicitly?
newtype Key a = Key a
unKey :: Key a -> a
unKey (Key k) = k
One downside I see is that importing/exporting Key(..) will no longer include unKey, which might be a problem for migrating an existing codebase.
I often find the un... name not very helpful – I see why @Swordlash uses runMyMonad, but to not at all include the name of the newtype seems … well, weird
What if some other newtype wraps a int64? What if some newtype wraps a pair of int64?
Yeah, the general direction that sounds nice to me would be something like -XNewtypeSelectors which generates selectors for datatypes, but not for newtypes. But I guess adding another extension just for that may be too much.