-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What's the status of the TF/HKT approach to avoiding excess type variables? #590
Comments
Hi @tysonzero, I'd be happy to help you get started with Opaleye. I wouldn't say that " I initial introduced Suppose we have a table data FooField =
FooField (Field SqlInt4) (Field SqlText) We also need to read it into Haskell values, so we also need a product type like data FooHaskell =
FooHaskell Int String When inserting, the primary key is optional (it can be specified as data FooFieldWrite =
FooFieldWrite (Maybe (Field SqlInt4)) (Field SqlText) but we probably want to specify Haskell values to insert, so we also need data FooHaskellWrite =
FooHaskellWrite (Maybe Int) String And that set up is absolutely fine by Opaleye. As long as you're willing to write the necessary The simplest "low technology" way to deal with the verbosity is to use a fully polymorphic product type for each of the four cases data Foo a b = Foo a b
type FooField = Foo (Field SqlInt4) (Field SqlText)
type FooHaskell = Foo Int String
type FooFieldWrite = (Maybe (Field SqlInt4)) (Field SqlText)
type FooHaskellWrite = (Maybe Int) String I wrote But this is not the only possibility! You could define a type family to make this job easier. That's why data Foo f = Foo (TableRecordField SqlInt4 Int Opt NN) (TableRecordField SqlText String Req NN) that defines an optional (i.e. type FooField = Foo O
type FooHaskell = Foo H
type FooFieldWrite = Foo W
-- type FooHaskellWrite = ? I'm not sure there was one for this Importantly, doing this does not require any access to Opaleye internals! It can be done purely by external clients. Again, Opaleye itself is completely agnostic. (This is one of the reasons that Of course, it's not necessarily helpful to be agnostic. Sometimes libraries should be opinionated and codify good practice. So my question to you is what do you really want to achieve here? If you just want to reduce boilerplate then I'm sure you could start with |
That all makes sense, thanks so much for the thorough response! To directly respond to your final question. I guess what I want to achieve is to write something close to what you have there:
I'm hesitant to use Is there existing material to do the above? I definitely wouldn't rule out contributing in this area in the longer term. However in the short term my time/resource constraints and my lack of familiarity with Opaleye make this unviable, so I just want to figure out the best setup I can get right now. I was leaning towards using rel8 which more or less fits with your external lib commentary since it's built on opaleye, however rel8 is a bit too segfault-y at the moment for me to use it. |
OK, well you can simply do (almost) exactly that! (See code below). There was a tutorial for the type family style, deleted in ad6f7f7. You can read the version before it was deleted. If you want to keep using this style then
What I need from you, if we're going down this route:
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE UndecidableInstances #-}
import Opaleye (Select, SqlText, SqlUuid, SqlTimestamptz, Table,
selectTable, tableField, table)
import Opaleye.TypeFamilies (Opt, NN, N, O, W,
Req, (:<*>), (:<$>))
import qualified Opaleye.TypeFamilies
import Opaleye.Internal.TypeFamilies (Arr, TC)
import Data.Text (Text)
import Prelude hiding (id)
import Data.UUID (UUID)
import Data.Time (UTCTime)
import Data.Profunctor.Product (ProductProfunctor)
import Data.Profunctor.Product.Adaptor (genericAdaptor)
import qualified Data.Profunctor.Product.Default as D
import GHC.Generics (Generic)
import Data.Kind (Type)
-- We should make this change in Opaleye, to restrict the
-- argument type of TableRecordField
type TableRecordField (f ::Arr Type (TC Type) Type) a b c d =
Opaleye.TypeFamilies.TableRecordField f a b c d
data Foo f = Foo
{ id :: TableRecordField f UUID SqlUuid NN Opt
, name :: TableRecordField f Text SqlText NN Req
, created :: TableRecordField f UTCTime SqlTimestamptz NN Opt
, notes :: TableRecordField f Text SqlText N Req
}
deriving Generic
-- I guess we can add this to Opaleye, for boilerplate reduction.
type AdaptorTF f =
forall p a b.
ProductProfunctor p
=> Foo (p :<$> a :<*> b)
-> p (Foo a) (Foo b)
-- No TH needed for the adaptor. Generic works fine.
pFoo :: AdaptorTF Foo
pFoo = genericAdaptor
-- I guess we can add this to Opaleye, for boilerplate reduction.
type FieldTF p h s n o a b =
D.Default
p
(TableRecordField a h s n o)
(TableRecordField b h s n o)
-- TH still may be needed here to write out the context
instance ( ProductProfunctor p
, FieldTF p UUID SqlUuid NN Opt a b
, FieldTF p Text SqlText N Req a b
, FieldTF p Text SqlText NN Req a b
, FieldTF p UTCTime SqlTimestamptz NN Opt a b) =>
D.Default p (Foo a) (Foo b)
-- Body derived by Generic
fooTable :: Table (Foo W) (Foo O)
fooTable = table "foo" . pFoo $ Foo
{ id = tableField "id"
, name = tableField "name"
, created = tableField "created"
, notes = tableField "notes"
}
fooSelect :: Select (Foo O)
fooSelect = selectTable fooTable |
In fact, to avoid the enormous context on the instance D.Default Unpackspec (Foo O) (Foo O)
instance D.Default FromFields (Foo O) (Foo H) Those are the specific instances you need to write fooSelect :: Select (Foo O)
fooSelect = selectTable fooTable
fooRunSelect :: Conn -> IO [Foo H]
fooRunSelect conn = runSelectTF conn fooSelect |
Thanks so much for the all the info and ideas! Hmm, the real solution here IMO is if Haskell had proper extensible anonymous records, however given the lack of them I am warming up a bit more to the fully polymorphic approach. Particularly considering you can always immediately define a type family based alias:
I skipped differing read/write types and nullability for simplicity, but adding it to the above is pretty trivial. This makes deriving various instances on I wish you could just bundle up |
Given the above I still think the
This seems pretty similar to the existing machinery, just cleaned up a little bit and with the inclusion of On a related note |
You're welcome!
Right. To put it another way: whatever style of record type Haskell supports conveniently, Opaleye also supports conveniently. As soon as there's a convenient Haskell solution for records with possibly-absent, possibly-
Yes, I'd suggest just getting started with that because it's the lowest-overhead way of becoming familiar with the library.
Indeed.
I think it there's potential in
Yes, that looks exactly like the kind of more polished API we'd need if we're going forward with this type families stuff.
The problem is that if you declare a field, say the primary key, |
Agreed on everything else, and yes I did dabble a little into that style of type family approach, and whilst I didn't quite get it to a nice spot, I may have just not tried hard enough.
Yes with the rename to This renamed |
Great
Ah, fair enough. I might need a bit more explanation of what you're imagining, but there's no rush since I won't deprecate it until a new major version of Opaleye and there's not one of those on the horizon! |
Here is an example of generated column usage and the associated opaleye:
should have:
Generated columns + multi-column on update cascade foreign keys let you do a wild amount of "denormalization" with pretty much none of the usual downsides, whilst keeping the performance benefits, and in some cases actually enforcing more constraints than a more traditionally "normalized" schema. |
Although with the above said, being able to truly "unhook" a field when calling
I know this has gone sort of off topic, but it all more or less relates to the core idea of taking the most advantage of this fully polymorphic product approach and making people less motivated to reach for the type family approach like I initially was. |
That's very impressive!
Yes, the fully polymorphic product approach affords a lot of flexibility. |
Based on the above I'm not sure the original issue really needs to be addressed. Additional documentation going through these trade offs could be worthwhile though to help new people who go through the same thought process as me. Perhaps Opaleye.TypeFamilies module could include the summary of the discussion and say something along the lines of "if you still want to use TypeFamilies after considering these tradeoffs, consider making a PR to add them here". I don't think I'll use type families myself at all, as the decreased verbosity isn't that big of a deal and the additional flexibility is nice. However I do think it's worth creating issues for a properly working "readOnlyTableField" and "missingTableField", and I guess even "writeOnly(Required|Optional|)TableField" to complete the diagram. If "readOnlyTableField" is fixed then renaming the existing implementation to "generatedTableField" is not necessary. |
Could you file a separate issue about this, describing all the combinations that are useful to support, in more detail? I still don't fully understand (although what you've explained so far convinces me there's potential in this area). |
Created the other issue! Ok so this issue in my mind is now just: "Document reasons for taking fully polymorphic approach over type families approach in Opaleye.TypeFamilies, with note that contributions are welcome for those that disagree and would like a cleaned up and more automated type family approach" |
I have been looking into alternative database libraries to use, as I've solely been using persistent/esqueleto for a while now. I like a lot of the Opaleye design but I have to say that the
a b c d e f ...
type variables are quite an eyesore, both directly and in the way that it seems like they will affect lots of the surrounding code and error messages and so on. I noticedOpaleye.TypeFamilies
exists but is deprecated, and then there isrel8
which builds onopaleye
and usesColumn f MyType
wheref
is generally some sort of functor. I'm assuming based on the lack of mention for this stuff in the tutorial that thea b c d e f ...
approach is the idiomatic way to do things, but I was wondering if there was a way to avoid that.The text was updated successfully, but these errors were encountered: