Is it possible to define your own Persistent / Esqueleto lens?

117 Views Asked by At

Given the following persistent type:

share
    [mkPersist sqlSettings, mkMigrate "migrateAll"]
    [persistLowerCase|
Account
    email Text
    passphrase Text
    firstName Text
    lastName Text
    deriving Eq Show Generic
|]

What I think is a kind of lens is generated, ie AccountEmail, AccountPassphrase etc etc. Is it possible to combine these in such a way, not necessarily composition but say string concatenation, I often find myself writing these kinds of functions:

accountFullName :: SqlExpr (Entity Account) -> SqlExpr Text
accountFullName acc = acc ^. AccountFirstName ++. val " " ++. acc ^. AccountLastName

It would be good if I could define this in a similar way to Account* so I can call them using ^. rather than using raw functions, ie acc ^. AccountFullName. This may not be an appropriate way of using these accessors, but if it isn't I would be interested to know why as I feel it may help further my understanding of this part of the Persistent library, as I feel fairly lost when I look at the code around EntityField...

1

There are 1 best solutions below

0
On BEST ANSWER

This isn't really possible.

(^.) :: (PersistEntity val, PersistField typ)
     => expr (Entity val)
     -> EntityField val typ
     -> expr (Value typ)

You'll see that the second argument is EntityField val typ, which is a type family defined in the PersistEntity val class. This is pre-defined for your table, and can't be changed, so this particular operator can't be used for custom accessors the way you want.


When you use persistLowerCase and friends, it uses Template Haskell to parse your definition and generate appropriate data definitions. As I understand it, something similar to the following is generated:

data Account = Account
  { accountEmail :: Text
  , accountPassphrase :: Text
  , accountFirstName :: Text
  , accountLastName :: Text
  }

data AccountField a where
  AccountId :: AccountField Key
  AccountEmail :: AccountField Text
  AccountPassphrase :: AccountField Text
  AccountFirstName :: AccountField Text
  AccountLastName :: AccountField Text

instance PersistEntity Account where
  data EntityField Account a = AccountField a
  ...

(This isn't syntactically accurate and is missing a lot of detail, but it provides enough context for this situation I think.)

So the "lens" you pass to (^.) is actually just a constructor for a type associated with your table type Account. You can't create new constructors or re-associate the type family dynamically, so you can't make something else that can be passed to (^.). These accessors are effectively set in stone.


I think it makes the most sense to just go with the raw function. accountFullName acc isn't bad to write, and it makes it clear that you're doing something a little more complex than just pulling a field value.