Lets say I have a datatype like the following:
data File' key generated value = File
{
id :: key Int,
md5Hash :: generated Text,
contents :: value Text,
description :: value Text
.
.
}
then I can then have things like this:
type FileCreate = File' (Const Void) (Const Void) Identity
type FileUpdate = File' Identity (Const Void) Maybe
type FileLookup = File' Identity (Const Void) (Const Void)
type File = File' Identity Identity Identity
But here's the annoying thing.
Lets say I want to do a file lookup, so I do it like so:
FileLookup { id = Identity 1234 }
Then I'm going to get warnings about unassigned record fields. I want to keep these warnings on, because if I do:
FileCreate { description = Identity "This file hasn't had 'contents' assigned so this should be an error" }
then this is an error, and I want the warning to alert me about this. But in the case of FileLookup
, the other fields are Const Void
. They have no valid values anyway, as Const Void
is newtyped to Void
.
Here my options seem to be:
fileCreate :: Text -> Text -> FileCreate
, fileUpdate :: Int -> Text -> Text -> FileUpdate
, fileKey :: Int -> FileLookup
etc, but this seems boilerplatey and I lose the nice record syntax.In my actual problem I'm likely to have lots of these "File" like datatypes but probably only around 5 patterns (insert, update, lookup etc). So happy to do some boilerplate for these patterns as long as I don't have to repeat it for every new datatype.
Happy to use Generics if it helps. Would like to avoid Template Haskell if possible but if it's the only way I guess I'll deal. But ideally I'd like something like
{-# PRAGMA DontWarnIfThisParticularTypeOrANewtypeOfThisParticularTypeIsNotInitialisedInARecord #-}
data MyConstVoid a -- no constructors
and just use MyConstVoid
instead of Const Void
(which I'm happy to do). I just can't find a warning suppressor like that.
The trouble is those fields are uninitialised (which mean they contain bottom). And you're relying on them containing bottom, since Const Void
has no non-bottom inhabitants. You want GHC to understand your intentions so that it can warn about some uninitialised fields but not others.
I suppose I can see why "this field can't be initialised (except to bottom)" might be expected to suppress a warning about not initialising the field. The trouble is that doesn't actually fit the idioms around how Void
is used.
In particular, your type says that getConst . value
applied to a FileLookup
is supposed to produce a value of type Void
. We've all agreed that once you have a Void
, you can use absurd :: Void -> a
to convert it to any type you like. For example, I was able to write this program using your suggested types:
import Data.Functor.Identity
import Data.Void
import Data.Text
import Control.Applicative
data File' key generated value = File {
id' :: key Int,
md5Hash :: generated Text,
contents :: value Text,
description :: value Text
}
type FileCreate = File' (Const Void) (Const Void) Identity
type FileUpdate = File' Identity (Const Void) Maybe
type FileLookup = File' Identity (Const Void) (Const Void)
type File = File' Identity Identity Identity
wat :: FileLookup -> anything
wat = absurd . getConst . contents
main :: IO ()
main = do
let q :: [Maybe (Either () [Double])]
q = wat $ File { id' = 123 }
print q
The only compile-time problem this has is the warning you want to disable:
Main.hs:26:17: warning: [-Wmissing-fields]
• Fields of ‘File’ not initialised:
md5Hash :: Const Void Text
contents :: Const Void Text
description :: Const Void Text
Calling contents
on a FileLookup
is perfectly fine by the type-checker, so long as we can handle the resulting Const Void
(which is easy to do, since we have absurd
). So if your desired functionality existed, this program would compile warning-free and then blow up at runtime. with Main: Main.hs:26:17-34: Missing field in record construction contents
.
The way Void
is expected to be used is more commonly to mark cases of a sum type that cannot happen. For example Either Void b
is a type that can only contain values of the form Right (_ :: b)
; to have constructed a Left Void
we would have first needed a value of type Void
to apply Left
to.2 Then either absurd
is a way to handle such Either Void b
values, relying on the assumption that either
will never take its Left
branch (and thus will never actually apply absurd
to anything) because no such value will ever exist, and checking that this case is "impossible" by requiring it to have type Void
(unlike handling an "impossible" case with something like error "will never happen"
, which will happily convert into a runtime bug when the types change so that the impossible case is now possible).
It's less common to see it in a purely product type, like yours, since that would indicate that that FileCreate
(etc) values will never exist, rather than indicating that certain of their fields will never be used.
Thus I think it unlikely that there would be any specific support your idiom built into the compiler, in the form of treating the "uninitialised field" warning differently based on the type of the field. Void
simply isn't designed to be used that way.
In fact, the fact that it's Void
rather than any other specific type isn't really doing anything. The only use you're getting out of it is that if someone accidentally tries to fill in the contents
of a FileLookup
they will almost certainly get a type error telling them about the mistake, since Const Void
won't match the type that would normally go in the contents
field (and the same thing if anyone tries to read such a field). But Const ()
isn't going to match their expected type either; nor would Const MyUnexportedType
, nor even something arbitrary and dumb like Const [Maybe (Either () [Double])]
. You aren't really getting any use out of the fact that Void
is uninhabited by simply leaving the field uninitialised; you could leave a field of any other unlikely-to-be-useful type uninitialised and get almost exactly the same type safety.
1 Of course Left undefined
works perfectly well to construct a value of Either Void b
that uses the Left
constructor. We basically trust that there is a "gentlemen's agreement" not to do things like that though; it's generally hard to accidentally bypass the type checker in that way, and Void
is specifically intended to support the style of programming where we try to ensure everything is total and invariants are checked by the compiler.
Maybe it's just the simplified example you've given here, but I also don't really see that you're getting "nice" syntax out of this pattern. FileLookup { id = 123 }
looks nice, but it doesn't actually work that way; FileLookup
is a type; record creation syntax is used with a constructor. So you actually have to use File { id = 123 }
; nothing says that it's a lookup. A FileDelete
would presumably also only need an id
and thus would also look like File { id = 123 }
.
Furthermore the { id = 123 }
bit only looks nice because there's a Num
instance for Identity
. There's an IsString
instance too, so if you're using OverloadedStrings
your Text
fields also probably look nice. But anything past that will make you start writing things like { flag = Identity True }
, and you'll still have to deal with adding and removing Identity
wrappers if you want these fields to connect to anything that deals in plain Int
and Text
values, even if you can have invisible Identity
wrappers with literals in your source code.
Really the pattern synonym version (where the unused fields are based on ()
rather than Void
) would be much nicer to use in practice. And creating a pattern synonym per operation per data type isn't much more boilerplate than creating a type synonym per operation per data type. Here's a quick example I knocked up:
{-# LANGUAGE PatternSynonyms, OverloadedStrings, DuplicateRecordFields #-}
import Control.Applicative
import Data.Functor.Identity
import Data.Text
-- same as before
data File' key generated value = File {
id' :: key Int,
md5Hash :: generated Text,
contents :: value Text,
description :: value Text
}
-- GHC couldn't figure out the constraints with a plain deriving clause on the
-- data type; this isn't important, it's just so I could demo usage in main
deriving instance (Show (key Int), Show (generated Text), Show (value Text))
=> Show (File' key generated value)
-- Unnecessary, but a decent amount of boilerplate reduction, and more easily
-- conveys the "disallowed" sense.
type X = Const ()
pattern X = Const ()
-- Note that here the id' value is a real Int, not an Identity Int. The
-- pattern can transparently apply/remove the Identity wrapper for us.
-- It's entirely up to you whether you do that, but I sure would.
-- (Using the real File constructor makes the Identity visible, of course)
pattern FileLookup :: Int -> File' Identity X X
pattern FileLookup { id' } = File (Identity id') X X X
-- Note that the type signatures for the pattern synonyms are optional
-- if you're averse to boilerplate; I've included them here for clarity
pattern FileUpdate :: Int -> Maybe Text -> Maybe Text -> File' Identity X Maybe
pattern FileUpdate { id', contents, description } = File (Identity id') X contents description
main :: IO ()
main = do
print $ FileLookup { id' = 123 }
print $ FileUpdate { id' = 123, contents = Just "foo", description = Nothing }
Here's what this prints, so you can see it's filled in the
File {id' = Identity 123, md5Hash = Const (), contents = Const (), description = Const ()}
File {id' = Identity 123, md5Hash = Const (), contents = Just "foo", description = Nothing}
And if you leave a field out, like FileUpdate { id' = 123, contents = Just "foo" }
, then you of course get an uninitialised fields warning:
Main.hs:36:11: warning: [-Wmissing-fields]
• Fields of ‘FileUpdate’ not initialised:
description :: Maybe Text
A big enough issue that I didn't want to bury it in comments in the code example: pattern synonyms with record syntax of course define their own fields and selector functions. So I had to turn on DuplicateRecordFields
to allow this, and if you try to use id'
as a selector you'll almost certainly get an ambiguity error.
What I would probably really do is define the pattern synonyms in a separate module with DuplicateRecordFields
and NoFieldSelectors
, and then use DisambiguateRecordFields
at the usage sites. Then you would only have to worry about qualifying them if you use record update syntax (not pattern matching or record creation) in a module where you both the real type and a pattern synonym (or two pattern synonyms) in scope.
Alternatively it might be nice to NoFieldSelectors
the real type as well, and only ever use explicit patterns. The pattern synonyms I've defined don't merely not use the unused fields, they actually enforce that the unused fields are Const ()
. If you consume these records (presumably in a module that's actually carrying the operations out) by writing patterns using File
directly (leaving the unused fields unmatched) or calling the field selector functions, then someone could initialise all fields and use that record with any operation, with no type safety. But if the FileLookup
etc patterns are used at the use sites as well as the creation sites, then that would be caught as an error. (Although if everything ends up completely separated, there's not much point in having a single record; separate data types for each pattern synonym would be simpler. Presumably there is some level where you handle these things generically without caring what kind of operation is in them)