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
Separate HasField into GetField and SetField #286
base: master
Are you sure you want to change the base?
Conversation
(The lnk "This proposal is discussed at this pull request." is broken.) To me this proposal looks sensible. I too am keen on virtual fields. I like the fact that I'd like to see default methods for all three methods, and a MINIMAL pragma. However I'm conscious that all this was discussed in the original SetField proposal. I have not re-read that disussion, but someone should, so that we don't reinvent the wheel here. |
Why not what I described here? I like this proposal more than the previous one, but it's just one tiny step away from supporting general setters that can look under
I did that and I didn't find the counterarguments convincing. See my comment. |
For the last proposal the biggest counter arguments against this idea were that the get and set could not be proven to be equivalent (much like Eq/Ord have a stated but unproven consistency condition). I'm happy either way, assuming the question about when such an instance is in scope can be resolved. Note that making a record field allow both of them to exist would be compatible with today, so might be not necessary to do better. |
The proposal nicely includes various methods in the
I would propose adding one more, for effectful modification:
With |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm generally in support of this proposal.
Why not what I described here? I like this proposal more than the previous one, but it's just one tiny step away from supporting general setters that can look under Maybe, set multiple fields at once, etc.
@effectfully do I understand correctly that the change you'd like to see to this proposal is removing the GetField
superclass of SetField
, to permit set-only "fields" (which could include traversing Maybe
)? We could keep GetField
/SetField
separate and (re)introduce another constraint HasField
to represent having both, without too much difficulty. But given that such general setters don't arise from Haskell records (excluding partial fields, perhaps), I wonder if one is better off using a lens/optics library for such cases?
proposals/separate-get-set-field.md
Outdated
Should any of `hasField`, `setField`, `updateField` be dropped from `SetField`? | ||
They are all mentioned currently for performance reasons, as I can imagine | ||
situations where it seems like you need all 3 for optimal perf. But they can | ||
all be defined in terms of each other, so perhaps some should be dropped. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a very strong opinion about which representation for setters we use (cunningly-disguised van Laarhoven lenses included). The situations in which there is a runtime performance difference are fairly arcane.
But I am slightly concerned that if the SetField
class is too large, the compiler might need to do a lot of work generating the dictionaries, reducing compile-time performance. Bear in mind that we need to do this for all fields of all record types GHC compiles. @simonpj is that a reasonable concern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation wise dictionaries could be created at use-site on the fly (current HasField
), or at record definition site (cf. selector functions).
Maybe even something one can control with some -f
switch: if you go all in for overloaded fields machinery, at record definition site would be better. But for occasional use at use-site is probably fine to generate dictionaries on the fly.
## Alternatives | ||
|
||
Separate out further into "get-only", "set-only", "get-0-or-more", | ||
"get-and-set" etc. instead of only "get-only" and "get-and-set". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do people feel about doing something like this snippet or #158 (comment) to better support partial fields? The cost is a bit more implementation complexity including an extra type parameter on the classes, and GetField
dictionaries no longer perfectly matching selector functions in the partial case, but the benefit is that we can define a sum-of-records and use it without the possibility of runtime crashes.
I generally agree that the GHC.Records
classes should be tied fairly closely to Haskell's records, rather than supporting more general optics, as it is easy enough to convert into a lens/optics representation when more elaborate constructions are needed. But as partial selector functions are in Haskell 98 (and are a bit of a wart, arguably), perhaps it is worth improving their treatment here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd raise a flag, that a time example used would like to have both, set-only
and get-only
fields.
You can get
day of a month from Day
, and you can modify
them in two ways (clip or rollover).
So with virtual fields IMO both read-only and modify-only are useful. There are cases (as above), where these cannot be combined into get-and-set
, but then they should have different names: problem solved.
Note: get-only fields is not really any closer to "Haskell records" than set-only fields, IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I think sum-of-records as currently implemented is a huge wart and I am ok with not really supporting it.
I would expect any reasonable implementation of sum-of-records to support the following:
data FooBar
= Foo { foo :: Int, baz :: Int }
| Bar { bar :: Int, qux :: Int }
fooBar :: FooBar -> Int
fooBar (Foo f) = f.foo + f.baz
fooBar (Bar b) = b.bar - b.qux
I am hoping that long term the above will be supported once anonymous rows/records come around. So in the short term I would personally prefer not to design features that would not be compatible with this approach.
sum-of-records will still be supported via NamedFieldPuns / pattern matching, which is compatible with having overlapping labels.
I would want support for sum-of-products to be in the form of some sort of proposal for sum's combined with this proposal, rather than trying to support them in this proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you fully explain what set-only fields you would expect for Day
, ideally in the form of working code, because it is very much not clear to me how you would end up with a legal set-only field that couldn't be made into a get-and-set field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am hoping that long term the above will be supported once anonymous rows/records come around. So in the short term I would personally prefer not to design features that would not be compatible with this approach.
I don't think partial fields cause any additional problems here, do they? Even with a single constructor / total field there would potentially be an odd mismatch between
foobar :: Foo -> Int
foobar (Foo f) = f.foo + f.bar -- possible future
foobar f = f.foo + f.bar -- with HasField as currently envisaged
although in principle I think this is all solvable if we have a separate type for anonymous records, because we could imagine something like
data FooBar
= Foo (Record { foo :: Int, baz :: Int })
| Bar (Record { bar :: Int, qux :: Int })
foobar (Foo f) = f.foo + f.bar -- works using HasField "foo" (Record {...}) Int Total
foobar f = f.foo + f.bar -- works using HasField "foo" FooBar Int Partial
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was envisioning a separate type, so both of the first examples would work.
-- works using HasField "foo" FooBar Int Partial
Hmm I'm not so sure how that would work, since Record
is an actual standalone type now, and thus it should be treated like any other type.
data FooBar
= Foo FooType
| Bar BarType
foobar :: FooBar -> ...
foobar f = f.foo
Under what circumstances would the above typecheck? You can't really do an "or" when it comes to instances, particularly not a clever "or" that gives you a different type when both match.
If the above was somehow made to work, it still seems to me as though the "sum" side of the "sum-of-records" should be handled using a sum-type proposal, and the "record" side would be handled by this proposal without consideration for the separately-handled "sum" side.
On a side note I think the complexity alone to support what I personally think is a substantial wart is enough for me to be against it.
Co-Authored-By: Adam Gundry <adam@well-typed.com>
Yes.
With general setters we get a quite more expressive system. My only concern is that some people might consider it too expressive.
Yes.
Well, the user probably doesn't want to use the record syntax and switch to lenses (and rewrite everything) once a single |
I agree, that having the possibility to implement So this would mean And then for performance reasons |
Generally I'm not too bothered about this. Typically there will be one, fixed dictionary for each top-level declared record type/field combination. Unlike |
Just
When it comes to pure updates, |
I‘d really want this feature, but I think it shouldn‘t come at the cost of the default use case. Here I assume that the default use case is someone implementing anything which they think of as a "real" field by implementing a Any ideas how to work around this? |
Via |
But note that this is not the primary use case. The primary use case is to derive efficient |
I don't personally see multiple fields of the same type as a "field", and GHC is also not going to derive these, so they would have to solely be virtual. This seems like something for a
Are they? I can't imagine all that many are used, since for decades now they have generated top level functions that simply crash on incorrect input. Personally I am hoping long term that sum-of-records is done using a sum of anonymous records, which would bring us back to having total fields.
Similar to above, seems like more of a To be clear I am not in any way against Particularly since all these proposals will make things nicer for the existing lens libraries, now that they don't have top level generated functions to avoid name collisions with. They can also utilize these classes for convenient field lenses that don't have to be defined in advance (often with TemplateHaskell).
It seems like you would still have to do that regardless once a single Tunneling through multiple layers where some of them are sum types seems like a job for
I agree, but I don't see it as something we should be asking I am fleshing out such a proposal, as it would be nice to have (although in our codebase records and field name collisions are far more frequent), it will probably involve |
Me neither.
I do not propose to have a
Yes, see the
You can specify the constructor explicitly as in instance s ~ s' => HasUpdate "just" (Maybe s) s' where
update _ = fmap
What I propose makes interaction with records/fields nicer and costs nearly nothing. So do you want to avoid the additional expressiveness just for the sake of it? |
Hmm, well given that this is in the I also think
I mean if the ask is solely to have:
Then I am not particularly opposed to it. I personally will continue using With that said I definitely do not want to use the complex I would personally love to have classes based around |
Yes, just rename |
Since this is module I also think
For consistency reasons alone
|
Yes, because it can be used to update the whole type. It's what the type signature of
This doesn't look composable to me. I do not care much about naming. |
Well then at least three people in this thread could agree on that. The question is if there is a better possibility to achieve the usage obtained by using On the other hand I don‘t see any huge cost related to the extended flexibility. |
That's why I think a separate proposal should be built around sum/variant-types and constructors, leaving this one focused on records/fields. I could imagine wanting things like I could even imagine a third proposal based around the inherent duality of records and variants, with things like deriving the ability to exhaustively pattern match on a variant using a record of the same structure.
Yeah I think set-only fields will have minimal benefit, but since the cost is low, I am personally open to the idea if that's what the community wants. |
So, what’s the way forward with this proposal? Leave it as is or change it to decouple |
I'm honestly not super sure. If these classes were built on top of a shared type family as follows:
I would feel a lot more comfortable with separate get and set classes. (It would also get rid of my need for This would also make it much nicer to build new hierarchies alongside the existing ghc-assisted classes, as you can be sure that all the new classes agree on the field types, regardless of the differences in what functionality is supported by them. It also seems like the above would make it much easier to derive lenses/optics on the fly, as you wouldn't have to worry what kind of traversal/lens to derive if As it stands I'm still not a fan of the idea of splitting up the classes, as the complexity and risk of divergence out-weigh the benefit which seems to be limited. |
Looks great to me. I like this version more than mine. So the updated code is (not tested) type family FieldType (x :: k) r
class GetField x s a where
getField :: FieldType x s -> s -> a
class HasUpdate x s where
update :: FieldType x s ~ a => Proxy# x -> (a -> a) -> s -> s
default update :: (GetField x s, FieldType x s ~ a) => Proxy# x -> (a -> a) -> s -> s
update x f s = setField x (f $ getField x s) s
setField :: Proxy# x -> FieldType x s -> s -> s
setField x = update x . const
type instance FieldType "just" (Maybe s) = s
-- No 'getField' possible.
instance HasUpdate "just" (Maybe s) where
update _ = fmap
data Twice a = Twice a a
type instance FieldType "twice" (Twice a) = a
-- No 'getField' possible.
-- (We could have a canonical instance for any 'Functor' and use @DerivingVia@)
-- (Or even just define generic @instance Functor f => HasUpdate "mapped" (f a)@)
instance HasUpdate "twice" (Twice a) where
update _ f (Twice x y) = Twice (f x) (f y) (although I'd rename
Well, that proposal is literally called "Separate
What do you mean? |
Hmm interesting. One thing I was planning on deferring until later proposals is some stuff related to prisms and constructors. But it is seeming quite relevant to the current discussion, so I will bring it up now. It seems clear to me that overloaded product-types / fields / lenses are useful, it also seems clear to me that overloaded sum-types / constructors / prisms are useful. From there we have at least two choices: Unified hierarchyIn this case we have a single type family that is used for both constructors and fields:
One advantage of this is that it is more unified. Constructors are reversed fields and vice versa. So However the naming might be hard when something is both a field and a constructor (newtypes/isomorphisms), such as Stratified hierarchyIn this case we have two type families, one for constructors and one for fields:
One advantage of this is it allows for variants to define passthrough instances for fields, which would otherwise cause problems with overlapping type families: There is the following style of set-only fields that only set when the constructor matches:
There is also the following style with get-and-set fields that work when multiple constructors share a field:
You could also do the dual of the above (records that define pass through instances for constructors), although it doesn't seem as useful. Both of these options have significant merit, and it's not a trivial decision. I probably lean towards stratified, since most will probably consider the "foo field" and the "foo constructor" of a type to be different, and this is really all about naming and namespacing. Plus we have two different syntaxes for the two things, so two different hierarchies seems to fit pretty seamlessly into that, I would classify current Haskell as stratified, due to all constructors having an uppercase first letter and all fields having a lowercase first letter. |
I would like to rule out the possibility of With a single type family this problem is avoided. This should also simplify things like building lenses and such on top of them, as well as various user-defined expansions to the hierarchy. Some might argue that they intentionally want divergence to increase flexibility, such as getting a field returning it in some context, (
to support:
Personally this causes me to want to stick to focusing on things that act like pure records, and thus keep the field types used in With the above said I think I am on the side of splitting up
For get-only fields it was about protecting invariants. For set-only fields it is about protecting information that would break the abstraction (or that is impossible to retrieve). It's fine to convert a (valid) AVL tree into a Set, but it exposes implementation details to let you convert back out. In the |
Ah. You elaborated that in the previous message, but then ended it with
so I thought that this "still not a fan" meant there were additional concerns.
Great examples. I do like the idea of using a single type family to retrieve the type of a field in both a getter and a setter. It might prove restrictive, but in this case we always can directly extend the functionality later. |
One slight complication is that the above examples only support |
@tysonzero good point. I wouldn't be against having separate Some fields are get-only, some fields are set-only, some fields can be updated. Those are distinct use cases and it makes sense to me that distinct use cases are handled by distinct type classes. |
Would that be:
or:
The former seems like it might be more practical? Unfortunately the former also means essentially skipping over |
@tysonzero I'd opt for 2 as it introduces a class that handles those problems that the other classes don't. I think it would be a good design (modulo the fact that I do not consider any solution that doesn't have a story for polymorphism a real solution). |
None of the steps seem unreasonable, but I'm a little concerned we've invented a big tower of type classes, without anyone saying they actually intend to use them. There are lens people, who don't want to use them because they don't express prisms. There is @effectfully who seems reluctant because of polymorphism. The two variant doesn't seem unreasonable, and would be used by @tysonzero. The I'm concerned about making something complex, people only using the simpler variant, but those people who implement or use the simpler variant having to grapple with additional complexity. I'd really like to hear from someone saying "I would actually use |
It's unfortunate that Haskell (along with every other mainstream language) makes it so hard to make changes like this in a fully backwards compatible manner. I would prefer to just start out with the type family stuff and The |
@tysonzero it seems a sad argument that we must go for maximum generality, not because we think it's useful, but because we can't prove its not useful and we only get one go. (Not saying that's a counter argument, merely that it seems unfortunate.) |
Honestly I really don't have a strong opinion on how much further to break up the hierarchy, or if the one I proposed is good enough. I adjusted the proposal to use a type family to make things a little nicer and to make future extensible / adjustments cleaner. What is the best thing to do at this point? Could I defer the decision about this to the committee, or perhaps we can shoot for the proposal as is and decide later whether or not to further split things up? |
In general, I feel that the proposal could do with more motivation or justification for the proposed design, and explanation of the trade-offs involved. Using a type family instead of a functional dependency is not a trivial change, and merits justification in the text. In particular see the discussion starting here on the previous proposal for an exploration of the issues involved, which resulted in a decision to keep using functional dependencies. It's conceivable that the decision should be changed, but at the very least the proposal should be explicit about what is being changed and why. This change is also more-or-less orthogonal to splitting |
Fair enough! Part of the motivation is that it allows library code to re-use this notion of the type of a field without having to declare The other motivation is that when playing around I have found Something quite uncontroversial and clearly terminating in
becomes the following:
Which requires The general rule of "instance heads (type family or class) should be simple and non-overlapping, outputs of type families and instance constraints can be complicated and overlap" no longer applies. As I brought up in #279 |
I don't know if this is a new idea, but as an alternative, we can get rid of SetField completely, and use GetField as SetField, by using a wrapper type whose virtual fields are partially applied setters. -- OverloadedLabels, fromLabel = getField for comments
data Person a = Person String Int a deriving (Show)
instance HasField "name" (Person a) String where getField (Person s _ _) = s
instance HasField "age" (Person a) Int where getField (Person _ i _) = i
instance HasField "data" (Person a) a where getField (Person _ _ d) = d
-- #name p
newtype Person_With a = Person_With (Person a)
instance HasField "name" (Person_With a) (String -> Person a) where getField (Person_With (Person _ i d)) s = Person s i d
instance HasField "age" (Person_With a) (Int -> Person a) where getField (Person_With (Person s _ d)) i = Person s i d
instance HasField "data" (Person_With a) (a -> Person a) where getField (Person_With (Person s i _)) d = Person s i d
-- #name (Person_With p) "new name"
instance HasField "with" (Person a) (Person_With a) where getField = Person_With
-- #name (#with p) "new name"
-- instance HasField "data" (Person_With a) (forall b . b -> Person b) where getField (Person_With (Person s i _)) d = Person s i d
-- -- what we really want The same idea could be used with other things, e.g. field names, metadata, lenses, traversals, whatever. Edit - this would also work: data Setter a
instance HasField (Setter "name") (Person a) (String -> Person a) where getField (Person _ i d) s = Person s i d
-- Setter but you know the type you're supplying
data Setter' a b
instance HasField (Setter' "data" b) (Person a) (b -> Person b) where getField (Person s i _) d = Person s i d
-- getField @(Setter "name")
-- setField @x = getField @(Setter x)
-- setField' @x (y :: a) r = getField @(Setter' x a) r y |
@tysonzero what's the status of this proposal? The current plan for |
I don't really have a strong stance on whether to keep it as is or to make I still would much prefer |
There are a variety of useful instances of
getField
that do not have acorresponding implementation of
setField
.These include records with invariants that
setField
could violate, likeTimeOfDay
orAsciiString
orCrossword
. As well as various uses ofvirtual fields that are completely impossible to set.
Due to dot-notation likely being built on top of
getField
, supporting thisincreased flexibility is of significant importance.
Rendered