Helper for records using type-level strings
Closed this issue · 13 comments
I think it would be a nice addition to Data.Vinyl.Derived
a function like:
(=.) :: forall proxy s a . KnownSymbol s => proxy s -> a -> FieldRec '[ '(s, a)]
(=.) p x = (Proxy :: Proxy '(s,a)) =: x
Which allows anonymous-like records to have well inferred types and be not too bad to write:
record =
Proxy @ "age" =. 27 <+>
Proxy @ "name" =. "me" <+>
Proxy @ "exists" =. False
Is there something like that already? (Maybe in other module?)
Something like this sounds good to me. I guess the Proxy
part makes it a little more verbose than would be ideal, but I can't think how to improve it while keeping the same general idea. I know this name is a conflict, but we could also say something like, SField @"age" 27
to denote a singleton record. Does that have similar appeal?
Either way, if you want to open a PR with the (=.)
suggestion, I'd be happy to review and merge it.
Yes, it sounds better, but for some reason, when using SField @ "age" =: 27
, there is always an error:
• Expected a type, but ‘"age"’ has kind ‘Symbol’
• In the type ‘"age"’
In the first argument of ‘(=:)’, namely ‘SField @"age"’
In the expression: SField @"age" =: 27
Or are you talking about something like:
field :: forall s a . KnownSymbol s => a -> FieldRec '[ '(s, a)]
field x = (Proxy :: Proxy '(s,a)) =: x
that allows field @ "age" 27
, which is nice too, even better.
Do you have a preference for the name of the function? Should I implement the operator from before? Just tell me and I can prepare the PR.
Thanks!
Yes, the latter is what I meant. I used the already-taken name SField
because this seems like a singleton record constructor. Your implementation is precisely what I had in mind. The name is worth a bit of thought: could we use a pattern synonym to make it look like an actual constructor? Another convention for smart constructors is the prefix mk
(so we'd have mkField
).
I have a minor preference to avoid operators unless they seem like a definite win, but I do use them a lot. In particular, does =.
conflict with anything likely to be used with vinyl
? It is used by persistent
and fclabels
, so perhaps it's not a problem today.
I've tried to come to a case where =.
is better fit than field
above, but I couldn't, so maybe it's not really necessary?
For the name, keeping it small would be nice, since I imagine people would be using it repeatedly. But mkField
seems ok too.
I couldn't come up with a pattern synonym for it, but that does not really bothers me, vinyl
is already heavy on type trickery and that could be one more layer of indirection for users.
A complement to field/mkField
above would be the equivalent for lenses, something like:
flens ::
forall s a rs f.
( KnownSymbol s
, RElem '(s, a) rs (RIndex '(s, a) rs)) =>
Lens' (Rec f rs) (f '(s, a))
flens = rlens SField
which allows the following:
ageRecord = field @ "age" 27
age = ageRecord ^. flens @ "age" @ Int . rfield -- 27
The second type application for the return seems to be necessary, I don't know if it is possible to get rid of it without defining custom lenses separately.
But these two give a good material for using vinyl
without any boilerplate, which seems a nice entry room for the library.
EDIT:
It's possible to reduce the work even more with:
flens' ::
forall s a rs.
(RElem '(s, a) rs (RIndex '(s, a) rs), KnownSymbol s) =>
Lens' (Rec ElField rs) a
flens' = flens @ s @ a @ rs @ ElField . rfield
Which would allow ageRecord ^. flens' @ "age" @ Int
but it needs AllowAmbiguousTypes
and it starts to look a little nasty...
@guaraqe This is what I do
type Person
= "firstname" =: String
$ "lastname" =: String
$ "age" =: Int
$ GNone
person :: MyRec Person _
person
= p @"firstname" "Bob"
:& p @"lastname" "Bill"
:& p @"age" 35
:& RNil
type IOPerson = FMap IO Person
ioperson :: MyRec IOPerson _
ioperson
= p @"firstname" (return "Bob")
:& p @"lastname" (return "Bill")
:& p @"age" (return 35)
:& RNil
type StringPerson = FConst String
stringPerson :: MyRec (FConst String) _
= p @"firstname" "Bob"
:& p @"lastname" "Bill"
:& p @"age" "35"
:& RNil
type PersonParser = FApply (->) (FConst String) (Person)
personParser :: MyRec PersonParser _
personParser
= p @"firstname" id
:& p @"lastname" id
:& p @"age" read
:& RNil
-- parsedPerson :: MyRec Person '["firstname", "lastname", "age"]
parsedPerson = rapply2 personParser stringPerson
-- age :: Int
age = parsedPerson^.rlens2 @"age"
Code to make the above work. I think this could be made polykinded so that it worked with any type and not just symbol.
{-# LANGUAGE PolyKinds, AllowAmbiguousTypes #-}
module VinylHelper where
import Data.Vinyl
import Data.Vinyl.Functor
import Control.Lens hiding (Identity, Const, getConst, rmap)
import Data.Singletons.TH
import Data.Kind
import Data.Vinyl.TypeLevel
import GHC.TypeLits
type family MyFun (a :: k1) :: k2
p :: (f ~ g) => MyFun (a f) -> MyAttr a f
p = MyAttr
newtype MyAttr a (b :: Symbol) = MyAttr { _unMyAttr :: MyFun (a b) }
makeLenses ''MyAttr
(=::) :: sing f -> MyFun (a f) -> MyAttr a f
_ =:: x = MyAttr x
data FConst (a :: *) (b :: Symbol)
data FApply (a :: * -> * -> *) (b :: e -> *) (c :: e -> *) (d :: Symbol)
data FMap (a :: * -> *) (b :: e -> *) (d :: Symbol)
type MyRec a b = Rec (MyAttr a) b
rapply2
:: MyRec (FApply (->) f g) rs
-> MyRec f rs
-> MyRec g rs
rapply2 RNil RNil = RNil
rapply2 (MyAttr f :& fs) (MyAttr x :& xs) = MyAttr (f x) :& (fs `rapply2` xs)
rlens2 :: forall r rs g a. (RElem r rs (RIndex r rs), Functor g) => (MyFun (a r) -> g (MyFun (a r))) -> Rec (MyAttr a) rs -> g (Rec (MyAttr a) rs)
rlens2 = rlens (Proxy @r) . unMyAttr
type instance MyFun (FConst a b) = a
type instance MyFun (FApply b c d a) = b (MyFun (c a)) (MyFun (d a))
type instance MyFun (FMap b c a) = b (MyFun (c a))
data GY (a :: k1) (b :: k2) (c :: k1 -> k3) (d :: k1)
data GNone (a :: k1)
type family GYTF a where
GYTF (GY a b _ a) = b
GYTF (GY _ _ c d) = MyFun (c d)
type instance MyFun (GY a b c d) = GYTF (GY a b c d)
type family GNoneTF (a :: k1) :: k2 where
type instance MyFun (GNone a) = GNoneTF a
type (a :: k1) =: (b :: k2) = a `GY` b
type (a :: j1 -> j2) $ (b :: j1) = a b
infixr 0 $
infixr 9 =:
rzipWith
:: (forall f. MyAttr a f -> MyAttr b f -> MyAttr c f)
-> MyRec a rs
-> MyRec b rs
-> MyRec c rs
rzipWith _ RNil RNil = RNil
rzipWith f (x :& xs) (y :& ys) = f x y :& rzipWith f xs ys
rzipWithC
:: (a -> b -> c)
-> MyRec (FConst a) rs
-> MyRec (FConst b) rs
-> MyRec (FConst c) rs
rzipWithC f = rzipWith (\(MyAttr a) (MyAttr b) -> MyAttr $ f a b)
rmapC
:: (a -> b)
-> MyRec (FConst a) rs
-> MyRec (FConst b) rs
rmapC f = rmap (\(MyAttr a) -> MyAttr $ f a)
@guaraqe Once we get to the two type applications, it seems like we're not doing much better than combining the label and payload together as in ElField
. We should try to figure out if it's possible to do better, but I'd be happy to move forward with field
or mkField
if you want to open a PR.
Then again...
@2426021684 That's great that you're able to get it down to parsedPerson^.rlens2 @"age"
! Do you think that approach could replace the current Derived
module? I'll have to read over it more closely.
@acowley Possibly, I don't know what your goals are. My code is a hack around Haskell not supporting the partial application of type families. My code needs better names, better type signatures, better kind signatures, and general improvements. The only downside (that I know of) of my approach is that the first argument to MyRec
must be specified. I believe that approach could work with type level strings and type level datas. It would break the current API. Note that rapply2
and rmapC
's type signatures do not work in all possible cases.
@acowley did you ever get a chance to read over it more closely?
any update? i've played around with TypeApplications and OverloadedLabels too, and while nothing is as convenient as a record literal, would be nice for this library to pick at least one of the convenience functions to export/document.
Yes, let's do it. If someone opens a PR, I'll be eager to merge.