-
Notifications
You must be signed in to change notification settings - Fork 62
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
Convert from newtype to lens #124
Conversation
Conflicts: src/Diagrams/CubicSpline.hs src/Diagrams/Points.hs
I'm excited to have the |
I suppose the real issue is not about breaking vs non-breaking changes (I pretty much always come down on the side of the "right" change, whether it is breaking or not), but the consistency of the user interface. If we want to switch to having some record accessors be lenses, then we ought to be consistent and change all record accessors to lenses. I don't want to be in the strange situation where users can never remember which record accessors are just normal projections functions and which ones are lenses. So, to summarize, the choices (as I see it) are:
The upside of (1) is that lenses are awesome and useful. The downside is that now diagrams users have to know a bit about lenses too. Though I would hope that with good documentation we can easily provide users with enough to be able to use them in a rudimentary way. Thoughts? |
Changing everything to lenses is pretty tempting. (2) might be frustrating, if we can't compose the new lenses with preexisting accessors. If we add a lens copy of every record accessor, we have lots of underscores, two ways to write every access, and more functions for new users to learn. But I haven't used lenses a whole lot. I don't have a good sense yet how hard to understand the type errors are. Type errors were my biggest frustration learning Haskell, and keeping Diagrams accessible to novice Haskellers seems worthwhile. |
I, on the other hand, do have a good sense of how hard to understand the type errors are, which I can now share: lens type errors are astoundingly incomprehensible. However, existing diagrams type errors are not exactly a walk in the park either. Getting good type error messages for embedded DSLs is an open research question. In any case, I typically allow diagrams design decisions to be driven by choosing power and flexibility over simplicity. If we can find good ways to make diagrams accessible to novice Haskellers, so much the better, but that is a secondary goal. So I really think we ought to go with (1). |
Exciting. Let me know how I can help. |
yes, me too. Helping out will force me to learn more about lenses! |
It's actually fairly straightforward. We can start, of course, by merging this pull request. Then look for any and all places (in -core, -lib, -cairo, -svg, -postscript, -contrib...) where we define a record and export its accessors. Change all the field labels to begin with underscores, and add a call to foo with { blah = baz, bar = quux } we will write things like foo (with & blah .~ baz & bar .~ quux) Perhaps we should even stop using I will let the two of you decide how to split up the work if you both want to do it. |
Convert from newtype to lens
@bergey It doesn't really matter to me how we split it up. Should we just go at it one file at a time and let each other know what we are working on via trello or do you want to pick -core, -lib, ... and i'll choose a different package? |
Trello sounds good to me. I made a trello card, with a long checklist of modules. I figure it's probably easier to do -core and -lib, then the things which depend on them. |
After implementing several of the -lib modules using lenses for the The code is uglier (IMHO) with more boilerplate. For example in Arrows where we now need only to export ArrowOpts(..) we will need to export ArrowOpts(ArrowOpts) and all 10 lenses we create for the record fields. We need to define all the field names with underscores, invoke Template Haskell with makeLenses. I like foo with { blah = baz, bar = quux } better than foo (with & blah .~ baz & bar .~ quux) This syntax can really be quite cumbersome for creating for example a highly customized arrow. Additionally, almost every existing diagrams program will break. I know that's not our primary concern, nevertheless it will be a nuisance. Is it worth giving some thought to changing all record accessors to |
Hmm, you have a point. So the idea would be to distinguish between optional-argument records (with a The only advantage of using lenses for optional argument records is if you wanted to "compute" with them, e.g. take an existing record of arguments and do something like multiplying one of the argument values by six. With lenses you could just say @bergey, what do you think? Can you see any other advantages to using lenses for optional argument records? |
I'm not bothered by the syntax change. I don't find it ugly, and it only adds one non-whitespace character per field set. @jeffreyrosenbluth makes a good point about the extra exports. I agree with @byorgey that the mane point is whether we'd like to say things like I'm OK either way. @byorgey, will you make the call? |
OK, in the spirit of "let's make this as powerful and flexible as possible", and seeing as it is impossible to predict what crazy/interesting things users will want to do (e.g. compute over options records), let's continue plowing ahead and converting everything to lenses, even optional argument records. And for the record, I don't mind the lens syntax either (at least, I find it no uglier than the record syntax). One more nuisance we haven't talked about, though, is the need to move all the Haddock documentation from the record fields themselves to the lenses. Attaching Haddock documentation to TH-generated lenses is a bit fiddly but it can be done. Here's how:
This is a bit annoying but (1) it's the only way to retain decent Haddocks and (2) it makes the code easier to read, too, since one can actually see what the TH is generating. |
OK, Let's finish the conversion first (since we have already begun) and On Mon, Oct 7, 2013 at 3:06 PM, Brent Yorgey notifications@github.comwrote:
|
Agreed. |
Argh!!! I can't get the type checker to cooperate when trying to build lenses for data ArrowOpts
= ArrowOpts
{ _arrowHead :: ArrowHT -- ^ A shape to place at the head of the arrow.
, _arrowTail :: ArrowHT -- ^ A shape to place at the tail of the arrow.
, _arrowShaft :: Trail R2 -- ^ The trail to use for the arrow shaft.
, _headSize :: Double -- ^ Radius of a circumcircle around the head.
, _tailSize :: Double -- ^ Radius of a circumcircle around the tail.
, _headGap :: Double -- ^ Distance to leave between
-- the head and the target point.
, _tailGap :: Double -- ^ Distance to leave between the
-- starting point and the tail.
, _headStyle :: HasStyle c => c -> c -- ^ Style to apply to the head.
, _tailStyle :: HasStyle c => c -> c -- ^ Style to apply to the tail.
, _shaftStyle :: HasStyle c => c -> c -- ^ Style to apply to the shaft.
}
makeLensesFor [ ("_arrowHead", "arrowHead")
, ("_arrowTail", "arrowTail")
, ("_arrowShaft", "arrowShaft")
, ("_headSize", "headSize")
, ("_tailSize", "tailSize")
, ("_headGap", "headGap")
, ("_tailGap", "tailGap") ] ''ArrowOpts
getHeadStyle :: HasStyle c => ArrowOpts -> (c -> c)
getHeadStyle (ArrowOpts {_headStyle = hs}) = hs
setHeadStyle :: HasStyle c => ArrowOpts -> (c -> c) -> ArrowOpts
setHeadStyle ao hs = ao {_headStyle = hs}
getTailStyle :: HasStyle c => ArrowOpts -> (c -> c)
getTailStyle (ArrowOpts {_tailStyle = ts}) = ts
setTailStyle :: HasStyle c => ArrowOpts -> (c -> c) -> ArrowOpts
setTailStyle ao ts = ao {_tailStyle = ts}
getShaftStyle :: HasStyle c => ArrowOpts -> (c -> c)
getShaftStyle (ArrowOpts {_shaftStyle = ss}) = ss
setShaftStyle :: HasStyle c => ArrowOpts -> (c -> c) -> ArrowOpts
setShaftStyle ao ss = ao {_shaftStyle = ss}
headStyle = lens getHeadStyle setHeadStyle
tailStyle = lens getTailStyle setTailStyle
shaftStyle = lens getShaftStyle setShaftStyle |
If you don't provide a type signature, what does it infer? Jeffrey Rosenbluth notifications@github.com wrote:
Sent from my Android phone with K-9 Mail. Please excuse my brevity. |
It can't infer the type. Could not deduce (t ~ (c -> c)) from the context (HasStyle c) bound by a type expected by the context: |
setHeadStyle :: ArrowOpts -> (forall c. HasStyle c => c -> c) -> ArrowOpts
setHeadStyle ao hs = ao {_headStyle = hs}
setTailStyle :: ArrowOpts -> (forall c. HasStyle c => c -> c) -> ArrowOpts
setTailStyle ao ts = ao {_tailStyle = ts} |
OK, I guess that works but If I try to use (^.) to access the ArrowOpt fields I still get all kinds of type check errors. I'm not very comfortable that this model works yet. |
!! Name Clash: & in both Data.Coordinates and Control.Lens. Also, we probably want to export Control.Lens ((&), (.~), (^.)) from diagrams-prelude so that all users don't have to import it. So I'm guessing we are going to need to rename & |
So it looks like @bergey and I have finished a first pass at converting -core and -lib to use lenses. After writing a descent amount of code to convert -lib to use Control.Lens I want to say one last time that I think a wholesale replacement of projections with Lenses as record accessors and mutators is a mistake. I'm just not convinced we are getting much extra power and flexibility. I agree that the lens are awesome but I don't think they are the best solution all the time. I have come across few if any places in
I don't often recommend throwing out lots of code I have worked hard to write. But in this case I suggest we rethink this one more time. |
You certainly make some valid points. Some responses below.
Yes, this is a bit annoying, but it's mostly a one-time cost for us, and will not in any way affect users of diagrams.
Another valid annoyance, but again, it doesn't affect users much if at all. (If users want to pattern match, I think that means we haven't given them enough high-level combinators.)
Very true. We will simply have to rename
The problem with
Very true.
What exactly is "the task"? Part of the point is that previously, we were using lightweight, ugly, inflexible machinery to accomplish certain things (constructing optional arguments records) which could not be easily extended to other things (computing over them). Now we are using heavy machinery indeed, but it is accomplishing a different task: what we end up with is more flexible and composable. In that sense the task is not so lightweight after all. I agree that we have not yet seen many interesting occurrences of computing over options records---but when all you have is a hammer, sometimes using a screwdriver doesn't even occur to you. If we do end up going with lensy options records, I'm willing to bet some enterprising users will eventually come up with some interesting uses for them. In the end I am still sold on the consistency of using lenses for all record accessors, and providing users with more flexibility. There's no denying that it certainly does come at a cost---but most of that cost consists of one-time pain for us (not users). (Where by "us" I mean, of course, "you"---thanks for all your hard work on this!!) The rest of the pain, being amortized over future development, I don't see as being a huge problem. (e.g. when writing a new module with an optional argument record, you have to be a little careful about the order of things because of TH, and you have to pull the strange trick for putting Haddock comments on the lenses, which will take slightly longer than just writing the Haddock comments directly on the fields.) Please don't interpret the above as shutting down the conversation; indeed, I'm really thankful to you for forcing us to think through this carefully, and I'm happy to continue the conversation as long as necessary. I am willing to serve as a "benevolent dictator" when absolutely necessary but I'd much rather see us all come to a consensus. Also, the one thing I haven't yet done is actually look carefully at the code changes, and it's always possible that might change my mind! We should also try converting some example code (e.g. in the user manual and so on) to see what it's like and whether there are any unexpected surprises. |
Thanks for your thoughtful response addressing all of my points. The Control.Lens is indeed an awesome package and using it for all of our record accessors gave me a great start in learning to use it. I agree that in the end most of the pain is for us, and that it is actually an improvement to the |
We were using the
newtype
package in several places; since it's now entirely superseded bylens
this patch converts from the former to the latter. This will be a breaking change since in particular the type ofpathTrails
is now different (it was a field accessor and is now anIso
). Maybe that's not a good idea and we should leavepathTrails
for backwards compatibility, and provide something likepathTrailsIso
instead? What do people think?There are probably still many ways the code could be cleaned up, we could make better use of what
lens
offers, etc.