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
record-set-field proposal #158
Conversation
CC @adamgundry |
A lens written directly can be more efficient than a lens written with
|
I also think that being able to set a field is a stronger condition (and not always needed) than being able to get it. Why not put it into a subclass? |
@int-index what type are you suggesting for |
Thanks for writing this up @ndmitchell! Perhaps GHC should simply provide separate |
I'm not sure. I expect
but there's no way to state that without
I guess the problem with this is that |
@int-index the idea here is to add the minimum stuff into GHC itself, in that sense it seems like two separate class IsLens (lbl :: Symbol) r a | r -> a where
myLens :: Lens' r a
default myLens :: (GetField lbl r a, SetField lbl r a) => Lens' r a But that's what Lens would add, and other libraries could feel free to go off in a different direction. @adamgundry if we go for two classes, my preference would be to rename the first, since it's not heavily used yet, and the lack of symmetry is disturbing. |
@ndmitchell What about adding both
I believe current code should continue to work if |
This proposal does not affect record update syntax, right? The OverloadedRecordFiels proposal describes how |
@winterland1989 no, anyone defining custom @nomeata this proposal does not affect record update syntax. The way HasField constraints are magically solved does not change at all, just the dictionary they produce has an extra field. Therefore, no change. (If we go ahead with splitting the class into two I'll note that both pieces should have the magic solving behaviour.) |
Thanks for the comments - I've updated the proposal, primarily to split the type class in two, and capture some of the alternatives that were mentioned. |
Note: Back when @adamgundry was working on the original records proposal the idea we floated was to have a This yields class SetField (x :: k) s t b | x s b -> t, x t -> b where
setField :: b -> s -> t which enables you to build field :: (HasField x s a, SetField x s t b) => Lens s t a b in full generality. This leaves only the concerns about higher rank fields, and situations like As an aside, I find the notion of a separate |
On type-changing vs. non-type-changing
@ekmett would you mind expanding on this "separate fix" a bit? The only workaround I'm aware of requires generalising the datatype e.g. to
Well, at least |
Unusual in current Haskell, but e.g. in C# it's a named feature with syntactic sugar ("properties"). It's already idiomatic to hide constructors and provide safe builder functions in Haskell. I think that if safe virtual-field lenses are currently rare, it might be more an issue of awareness and/or convenience than anything else. Providing a mechanism for them in base helps address both. That being said, I completely understand the current hesitance to embrace lenses in base at all. That remains the strongest argument, IMO. |
@ekmett good points, and one that points towards a more complex solution, with a fair bit more alternatives to consider. My inclination is that it is better to go for something simple, which doesn't preclude adding more complex alternatives later, rather than to avoid adding anything because it could be generalised. It's quite possible a separate @tejon I appreciate the desire for virtualising fields, but that's somewhat orthogonal - this proposal and the existing |
I would argue that there're very few people defining custom For baking van Laarhoven lenses, we shall bring a discussion on that but IIRC the community is leaning towards including it. e.g. http://r6.ca/blog/20120623T104901Z.html
It's a pity not to include it given how simple and elegant the type is. |
I'm not sure that's the right choice: for a deeply-nested field, Is implementing only BTW I agree with not binding Lenses into Haskell base. Contra @winterland1989, I imagine an alternative history of Haskell in which TRex became the record syntax. (They work well in Hugs, and knock spots of anything in GHC.) Or there were several ideas swirling around in 1996. |
I believe that one of the attractives of
I would argue that van Laarhoven lenses fit better with current Haskell, where |
Agreed.
Well, following this line of reasoning, what if the update is effectful? That is, we need to climb down the structure to get the current value, apply Then the primitive should be:
but of course, there's no reason to limit ourselves to
and that's a van Laarhoven lens. We can get |
@int-index that's a helpful line of reasoning, thanks. I've been talking this over with some of my colleagues at Well-Typed (special thanks to @edsko and @bgamari), and have come round to the idea of using the van Laarhoven representation in the payload of a class HasField x r a => SetField (x :: k) r a | x r -> a where
updateFieldF :: Functor f => (a -> f a) -> (r -> f r) (modulo name bikeshedding, the question of whether to support type-changing update, and whether we want to make this part of The point is that the use of the van Laarhoven representation can be regarded as an implementation detail (albeit one that will be particularly convenient for users of the I still think the cases in which making use of this representation really gives an efficiency benefit will be comparatively rare, as they necessarily involve custom |
Having I defer to others on the type of set/update. I agree that it's not worth adding a lot of complexity to support type-changing update. If the mono-typed version is simpler, let's do that. Are you sure we want to use functional dependencies here? Its there a reason not to use type families? The latter gives you fewer type parameters, and generally more equality evidence is available. |
This is sweet ! |
@danidiaz, sorry I can't resist this pun
I think you're talking eyewash: no reason I can see why Lenses couldn't be defined over TRex style records. The library of optics would be 'OpTRex' ;-). |
For type-changing updates, what about combining multiple fields into a sub-record or tuple? data FooBar a = FooBar { _foo :: a, _bar :: a }
foo, bar :: Lens (FooBar a) (FooBar a) a a
foo_and_bar :: Lens (FooBar a) (FooBar b) (a, a) (b, b)
-- x { foo = ... } -- uses foo
-- x { bar = ... } -- uses bar
-- x { foo = ..., bar = ... } -- uses foo_and_bar, possibly type-changing
foo = foo_and_bar._1
bar = foo_and_bar._2 Perhaps with some magic setFields class? This could have problems when combining compiler and user defined fields; it would be much more complicated to support. Edit: I just found a section of a blogpost from 2012 suggesting this:
|
Trying to summarise back towards a conclusion. I think it's important to understand the motivation for these primitives - they are a way to make use Records that is wired into the compiler. They are not intended as an end user API, but something with which such an API can be built. Think C FFI calls, simple, direct, which are then wrapped with all the user richness people expect (likely in many different ways, including by the Lens library). Q: Should we allow type changing lenses? No - this results in additional implementation complexity. Let's aim to get something through, rather than nothing, and a future dedicated soul can extend it. Q: Should we have HasField as a supertype of SetField? My view is no - that makes it a nicer concept to use directly and state laws about, but those aren't necessary to wire into primitives. However, I'm flexible. Q: Should we make it possible to define updates where the set/get pair can be more efficient? For actual records, it's unnecessary, since the operations are cheap anyway. But since we allow users to write Q: If we do make set/get more efficient, how should we do it? One way is to define it as a van Laarhoven lens. Another is to define: class SetField (x :: k) r a | x r -> a where
-- | Update function to set a field in the record.
setField :: r -> (a, a -> r) I prefer the simpler one, because of the simplicity. If the operation is generalised to be a superset of |
My first thought was summaries (e.g. a list stored with its length or total*), but, on second thought, why it would matter that these are get-only fields and not just regular functions? I think this applies in general - what's the difference between a get-only field and a regular function? Namespacing**? data LenList a = LenList { list :: [a], len :: Int }
-- invariant: len == length . list; len is O(1) * with dependent or refinement types, this may be another aspect of type-changing update |
I disagree. The choice of representation is intended to be an internal implementation detail, so we should expose More generally, it would be good to make progress here. I'm relatively ambivalent as to the exact answers to @ndmitchell's questions, but let's try to reach some sort of consensus and submit this to the committee. Or at least we could clarify the options remaining, as we could then ask the committee to make a decision between them. It seems to me that the main point of contention is whether a
What have I missed? |
Ok. I prefer this approach, but if it doesn't end up being implemented, then I'll try to improve the approach taken by this proposal and if all works out, I'll make another proposal. |
|
I wrote a blog post on various options regarding type-changing update, including a novel solution. |
@effectfully Impressive, the circular trick is nice! Also you should note that So i think what you do, phrased as FunDep would be class HasField name t s b a => HasField name s t a b | name s -> a, name t -> b where field :: Lens s t a b And instances should only case on instance (t ~ Proxy y, a ~ (), b ~ ()) => HasField "proxy" (Proxy x) t a b where field = lens (const ()) (\_ _ -> Proxy)
instance (t ~ (a', b), x ~ a, y ~ a') => HasField "_1" (a, b) t x y where field = _1 Prelude Optics Data.Proxy> :t set (field @"proxy") () (Proxy :: Proxy Int)
set (field @"proxy") () (Proxy :: Proxy Int)
:: forall k2 (y :: k2). Proxy y Prelude Optics Data.Proxy> :t over (field @"_1" % field @"_1") length (("abc", True), 'd')
over (field @"_1" % field @"_1") length (("abc", True), 'd')
:: ((Int, Bool), Char) I didn't try the type-changing type-family update though. When I tried to implement the polymorphic variant of this proposal, this is When it sees
This would avoid seemingly circular definition, as GHC is free to look at the structure of That's a direct extension of what is now done for current
And some technicalities due to types with multiple constructors, for example. To say it directly: However, it's possible to get better inference with fun-deps. And it's quite simple one, a working example: % ghci
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Loaded package environment from /home/phadej/.ghc/x86_64-linux-8.6.5/environments/default
Prelude> :set -XTemplateHaskell -XOverloadedLabels -XDataKinds -XFlexibleInstances -XFlexibleContexts -XMultiParamTypeClasses -XGADTs -XUndecidableInstances
Prelude> import Optics
Prelude Optics> data User = User { userEmail :: String, userName :: String }
Prelude Optics> ; $(makeFieldLabels ''User) Prelude Optics> over #email _ (User "john@gmail.com" "John")
<interactive>:5:13: error:
• Found hole: _ :: [Char] -> [Char]
• In the second argument of ‘over’, namely ‘_’ Prelude Optics> :t \f -> over #email f (User "john@gmail.com" "John")
\f -> over #email f (User "john@gmail.com" "John")
:: ([Char] -> [Char]) -> User Prelude Optics> :t over (_1 % _1) length (("abc", True), 'd')
over (_1 % _1) length (("abc", True), 'd') :: ((Int, Bool), Char) See Structure of LabelOptic instances section in https://hackage.haskell.org/package/optics-core-0.2/docs/Optics-Label.html The phantom arguments don't work though. I didn't try type-changing type-family update though. |
@phadej that's a ton of helpful info, thank you. I'll reference your reply in the post.
Yeah, this does look equivalent as you're basically encoding the
I think it would be hard to make an instance with polymorphic recursion by accident? And does the blow up-mean that the compiler will just give up after a certain amount of attempts to resolve the constraints? If not, can we do a silly thing and add an additional
Sorry, I don't get it. Why? The algorithms you outlined all seem to resolve
That's impressive, but how did you manage to achieve that? Looking at instance (a ~ String, b ~ String) => LabelOptic "name" A_Lens Human Human a b where
labelOptic = lensVL $ \f s -> (\v -> s { humanName = v }) <$> f (humanName s) I do not see how that can lead to working type inference. Could you please explain? You've shown the bottom-up type inference. Does the top-down one also work? I.e. what I write as -- Found type wildcard ‘_’ standing for ‘User’
test1 :: _ -> User
test1 user = user & lens @"name" .~ "new name" in the post.
I've only provided this example to show that the new approach doesn't break here unexpectedly. They all seem to perform equally well in the polymorphic case. |
In short: by accident is less strong than never.
I don't know what GHC does, whether it gives up or loops. (Probably gives up, but that would be a bug IMO).
That's explained in Structure of LabelOptic instances https://hackage.haskell.org/package/optics-core-0.2/docs/Optics-Label.html By leaving
Looks like it works: -- I tell only the result type:
*Main> :t \user -> set #name "new name" user :: User
\user -> set #name "new name" user :: User :: User -> User
Maybe I'm wrong, I never tried to resolve type families equalities in the type-checker (plugins). If you can assert that indeed
-- this one ---v v--- and it depends on name and s, e.g.
class LabelOptic (name :: Symbol) k s t a b | name s -> k a, name t -> k b, name s b -> t, name t a -> s where This prohibits you from having field (Lens) and constructor (Prism) of the same name, but they are distinct anyway. One class to rule them all. The type family approach has a downside: user can define
I don't think anything changed in that regard since 2015 |
I tried to (trivially) break undecidable instances, but obviously it doens't work. class HasField (name :: Symbol) t s b a => HasField name s t a b
| name s -> a
, name t -> b
where -- identity
newtype Foo a = Foo a
instance (t ~ Foo (Maybe x), a ~ x, b ~ Maybe x) => HasField "foo" (Foo x) t a b where errors with
instance (t ~ Foo (Maybe x)) => SameModulo "foo" (Foo x) t where errors with
I hope there's a simple argument that you cannot escape that loop, If it's unescapable, that's good :) In fact, I have written type-classes which are specialised versions of: class Convert a b | a -> b, b -> a where
convertTo :: a -> b
convertFrom :: b -> a But then I can rewrite them as class Convert b a => Convert a b where
convert :: a -> b in cases where having one name for |
Agreed.
Yeah, I understand the trick and I use it pretty often. What I don't understand is how it helps in this case. So consider this example *Main> :t \user -> set #name "new name" user :: User
\user -> set #name "new name" user :: User :: User -> User Things that are known to the compiler:
None of these fundeps
allow to resolve Ok, I'll play with that myself.
By the time when you start unifying But I have no idea how Haskell's type checker works, that's just my intuition developed while programming in Agda and writing unifiers. Anyway, fundeps are fine, it's not very important here whether fundeps or type families are used.
Nice.
Maybe, but I had a lot of trouble with associated type families not computing because of unresolved constraints or whatever. Making those type families unrestricted often helped, so now I just use them by default.
|
You need to implement that computation in the type-checker, as the goal (of this proposal) is to automatically create required instance dictionaries / type family equations on the fly. In GHC type families are (to some approximation, my best understanding) a list of equations, and we'd need to populate that list. User defined type-families are finite (and usually small) list of equations, but e.g. *Main> :kind! forall x. x + 0
forall x. x + 0 :: Nat
= x
*Main> :kind! forall x. 0 + x
forall x. 0 + x :: Nat
= x work. As inside GHC we actually can backtrack, for example, but if we tried to do similar thing in source language: type family Plus ... where
Plus 0 x = x
Plus x 0 = x then that won't reduce in Continuing to class HasLens name s t a b where
... without any fundeps, but resolve it in a way it had. I.e. create additional equality constraints to be satisfied. And we can (should!?) do the best possible thing, incl. possibly doing something which isn't really implementable in the source GHC Haskell, but still has definable algorithm (as my described, first look at Note, we are talking about GHCs type checker. "Haskell" don't even have MPTCs :) I think that's |
Okay, I think I know why inference works with fundeps. So we have the following fundeps:
but when we define an instance like instance (a ~ String, b ~ String) => HasLens "name" User User a b where
lensAt _ f (User email name) = User email <$> f name these two fundeps:
essentially become
because the instance head says that the instance matches any instance (a ~ String, b ~ ()) => HasLens "name" User () a b where
lensAt _ f (User email name) = f name results in I did not expect that! Should have checked before writing my post. Ok, thanks a lot @phadej, I'll reflect in the post that the solution presented there is worse than the already known fundep solution. |
For the record, |
Hm, in fact yes, functional dependencies can't handle phantom types and type families unlike the data Ph (bs :: [Bool]) = Ph { foo :: Int }
instance (a ~ Int, b ~ Int) => HasLens "foo" (Ph bs) (Ph bs') a b where
lensAt _ f (Ph i) = Ph <$> f i type family Goo (a :: k)
data Tf (a :: k) = Tf { bar :: Goo a }
instance (a ~ Goo x, b ~ Goo x') => HasLens "foo" (Tf (x :: k)) (Tf (x' :: k')) a b where
lensAt _ f (Tf x) = Tf <$> f x Back to editing the post... |
I updated the post reflecting the comments made by @phadej (thank you!). Main things relevant to this discussion are:
class SameModulo (x :: k) s t where
lensAt :: (SameModulo x t s, a ~ Get x s, b ~ Get x t) => Proxy# x -> Lens s t a b and everything will still work
data Ph (x :: k) (bs :: [Bool]) = Ph { foo :: Int }
instance (a ~ Tagged ('(,) x bs) Int, b ~ Tagged ('(,) x' bs') Int) =>
HasLens "foo" (Ph (x :: k) bs) (Ph (x' :: k') bs') a b where
lensAt _ f (Ph i) = Ph . unTagged <$> f (Tagged i)
ph :: Lens (Ph (a :: k) bs) (Ph (a' :: k') bs') Int Int
ph = lens @"foo" . coerced
|
Could you say a bit more about this? I'm not sure exactly what you mean. (I haven't been following very closely). |
Currently what is accepted is class HasField x r a | x r -> a where
hasField :: r -> (a -> r, a) This doesn't support type-changing update, e.g. you can write However making the class support type-changing update is only a matter of turning it into (which indeed can be done later, but I'm not sure why do it later, when you can just do it now) class HasField x s t a b | x s -> a, x t -> b, x s b -> t, x t a -> s where
hasField :: s -> (b -> t, a) and I'm not aware of any disadvantages that this definition has over the monomorphic version. I still think that the decision to accept the proposal was premature. @ekmett has a good argument in favor of separating |
So when you say "it can be directly extended to the one with functional dependencies" by "it can be" you mean "it can be by breaking the currently accepted API", right? |
Yes (by breaking it either by unaccepting it or in a separate proposal). |
I said it before, but it's worth repeating, The kind argument in |
I'm not fully convinced on this one. One useful aspect of separating the namespaces is it allows for pass-through instances. For example if you have some sort of generic sum type like The dual of the above would be defining constructors for generic products if every member of the product shares that constructor. This seems unlikely to be particularly useful but it's worth mentioning. I also think it would be rather confusing to not use separate namespaces in the case of newtypes. If I have If I see:
I would not expect |
But -- technically both are Isos, but for an example sake they aren't
-- generated with makeFieldLabels
instance LabelOptic "runIdentity" A_Lens (Identity a) (Identity b) a b where
labelOptic = ...
-- generated with makePrismLabels
instance LabelOptic "_Identity" A_Prism (Identity a) (Identity b) a b where
labelOptic = ... See also #270 (Note: |
Sure. But this is not confusing right now because they have very different types:
In this case they would both have the exact same type. This would mean I could do:
Which would be rather confusing. And yeah I know |
This proposal recommends improvements to the design of the `HasField` typeclass (differing from those planned under ghc-proposals#158). In particular, the proposed design supports updates using a new `SetField` class, adds support for unlifted datatypes and fields, and specifies laws for the classes. In order to keep this proposal simple, it does not yet propose adding support for type-changing update, which is left for a future proposal.
The separate "lens-labels" package hasn't been used outside of proto-lens. There's also a confusing distinction between regular lenses and the newtypes in `Lens.Labels` -- which, again, aren't widely used. - `Lens.Labels` -> `Data.ProtoLens.Fields` - Defines the `HasField` class. - Also defines a new `field` function that works with `TypeApplications`. - Removes the `LensFn` newtype and related functions. - `Lens.Labels.Unwrapped` -> `Data.ProtoLens.Labels` - Defines the orphan `IsLabel` instance. - `Lens.Labels.Prism` -> `Data.ProtoLens.Prism` - Defines prisms and related functions. Also rename `HasLens` to `HasField` to track the recent GHC proposal: (ghc-proposals/ghc-proposals#158 Also bump proto-lens-protobuf-types to version 0.5 since it's now more dependent on the new version of proto-lens.
The separate "lens-labels" package hasn't been used outside of proto-lens. There's also a confusing distinction between regular lenses and the newtypes in `Lens.Labels` -- which, again, aren't widely used. - `Lens.Labels` -> `Data.ProtoLens.Fields` - Defines the `HasField` class. - Also defines a new `field` function that works with `TypeApplications`. - Removes the `LensFn` newtype and related functions. - `Lens.Labels.Unwrapped` -> `Data.ProtoLens.Labels` - Defines the orphan `IsLabel` instance. - `Lens.Labels.Prism` -> `Data.ProtoLens.Prism` - Defines prisms and related functions. Also rename `HasLens` to `HasField` to track the recent GHC proposal: (ghc-proposals/ghc-proposals#158 Also bump proto-lens-protobuf-types to version 0.5 since it's now more dependent on the new version of proto-lens.
The separate "lens-labels" package hasn't been used outside of proto-lens. There's also a confusing distinction between regular lenses and the newtypes in `Lens.Labels` -- which, again, aren't widely used. - `Lens.Labels` -> `Data.ProtoLens.Fields` - Defines the `HasField` class. - Also defines a new `field` function that works with `TypeApplications`. - Removes the `LensFn` newtype and related functions. - `Lens.Labels.Unwrapped` -> `Data.ProtoLens.Labels` - Defines the orphan `IsLabel` instance. - `Lens.Labels.Prism` -> `Data.ProtoLens.Prism` - Defines prisms and related functions. Also rename `HasLens` to `HasField` to track the recent GHC proposal: (ghc-proposals/ghc-proposals#158 Also bump proto-lens-protobuf-types to version 0.5 since it's now more dependent on the new version of proto-lens.
The proposal has been accepted; the following discussion is mostly of historic interest.
This is a proposal to add the function
setField
to the built-in typeclassHasField
, allowing type-based resolution of field names in record update functions.Rendered