With the new OverloadedRecordDot
language extension, we can use the .
character to access stuff on records.
{-# language OverloadedRecordDot #-}
data User = User { name :: String }
main :: IO ()
main = do
let user = User { name = "Matt" }
putStrLn user.name
This is syntax sugar for the following code:
import GHC.Records
data User = User { name :: String }
instance HasField "name" User String where
getField (User n) =
n
main :: IO ()
main = do
let user = User { name = "Matt" }
putStrLn (getField @"name" user)
As it happens, we can add fields to a record.
{-# language OverloadedRecordDot #-}
data User = User { name :: String }
instance HasField "age" User Int where
getField user =
32
main :: IO ()
main = do
let user = User { name = "Matt" }
print user.age
This works, though it’s a bit boring.
It’s much more useful to have, say, virtual fields.
data User
= User
{ firstName :: String
, lastName :: String
}
instance HasField "name" User String where
getField user =
unwords [user.firstName, user.lastName]
This gives us a “virtual field,” which can allow us to refactor code that depends on the record field in neat ways!
So, those types, they don’t have to be ordinary values. They can be methods.
Or, y’know, functions, whatever, it’s all the same in Haskell.
instance HasField "greet" User (String -> IO ()) where
getField self message = do
putStrLn $ concat [message, ", ", self.name, "!"]
main :: IO ()
main = do
let user = User { firstName = "Matt", lastName = "Parsons" }
user.greet "UhhhHHH Excuse me WTF"
This prints out UhhhHHH Excuse me WTF, Matt Parsons!
.
Which is pretty cool.
impl
Rust has a keyword impl
, which is used in two ways:
The linked docs tell the whole story, just about.
I don’t know about you but I want nicer syntax than all the instance HasField
stuff.
I wrote a little library that should do this:
data User = User { name :: String }
impl ''User [d|
greet :: String -> IO ()
greet message = do
putStrLn $ concat [message, ", ", self.name]
|]
It’s relatively straightforward. In pseudocode, it’s implemented like this:
impl :: Name -> Q [Dec] -> Q [Dec]
impl tyName qds = do
decs <- qds
let
namesTypesExprs :: [(String, Type, Exp)]
namesTypesExprs =
getTypesAndExprs decs
instances <- for namesTypesExprs $ \(name, typ, exp) -> do
[d|
instance HasField $(name) $(tyName) $(typ) where
getField self = $(exp)
|]
pure (concat instances)
Unfortunately, I ran into a bit of a blocking issue, namely that GHC does not support OverloadedRecordDot
in TemplateHaskell
QuasiQuotes
yet.
While I can work around it, I’d rather not bother until OverloadedRecordDot
is fully supported by GHC.
There’s no polymorphism allowed.
Like, at all.
You can’t write:
instance (Show a) => HasField "myPrint" User (a -> IO ()) where
getField self a = do
putStrLn (show a)
This fails the functional dependencies.
You can’t write methods generic in MonadIO m => HasField User (String -> m ())
either.
The functional dependencies seem pretty reasonable:
class HasField sym r a | sym r -> a where
getField :: r -> a
This means that the types sym
and r
uniquely determine the a
type - or, that if you know what sym
and r
are, then you always know exactly what a
is.
Since users of our isntance are able to select things like IO
, ReaderT () IO
, and StateT Int IO
for this, you can’t uniquely determine the result type just based on the answer.
Seems like ImpredicativeTypes
should work here, but they apparently don’t.
instance HasField "myPrint" User (forall a. Show a => a -> IO ()) where ...
This fails with a syntax error, due to the way OverloadedRecordDot
affects GHC’s parser.
instance HasField "myPrint" User (forall a . Show a => a -> IO ()) where ...
This fails because it is an illegal polymorphic type.
But wait - this is an impredicative type.
ImpredicativeTypes
was a deprecated language extension, but I recall hearing that we landed support for them with Quick Look Impredicativity.
And, in GHC 9, we have a proper ImpredicativeTypes
behavior!
We definitely are paying a big cost for it - the Simplify Subsumption proposal gives no practical benefit to programmers except that it gives additional power to Quick Look Impredicativity.
Unfortunately, enabling ImpredicativeTypes
doesn’t make this work - GHC still deems the above an illegal polymorphic type.
It turns out, you can’t put an impredicative type in an instance at all.
Oh well.
Okay, so I posted this, and was immediately offered a Prime Tip by Sandy Maguire. Apparently Richard Eisenberg has published a video stating how to defeat this. The answer is to demand the constraint in the context.
So we can write myPrint
like this:
instance
( Show a
, HasField "myPrint" User (a -> IO ())
)
=>
HasField "myPrint" User (a -> IO ())
where
getField self a = do
putStrLn $ concat [self.name, " says: ", show a]
That let’s us write code like this:
go :: IO ()
go = do
let user = User { name = "Matt" }
user.myPrint 'a'
user.myPrint 3
Which evaluates like this:
$> go
Matt says: 'a'
Matt says: 3
Nice!
Deal unbreaker.
Reddit user /u/WhisterPayer
implemented this plugin to accomplish this!
Awesome.