Skip to content
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

RecordDotSyntax language extension proposal #282

Open
wants to merge 21 commits into
base: master
from

Conversation

@shayne-fletcher-da
Copy link
Contributor

shayne-fletcher-da commented Oct 11, 2019

We propose a new language extension RecordDotSyntax that provides syntactic sugar to make the features introduced in the HasField proposal more accessible, improving the user experience.

Rendered

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Oct 11, 2019

The link is broken in the rendered proposal, where it says "This proposal is discussed at this pull request"

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Oct 11, 2019

I strongly support the direction of travel of this proposal. I've wanted to use dot-notation for record selection since forever; and this looks like a very plausible way to do so. The fact that it's been used extensively in a production context helps reassure me that there aren't unexpected consequences.

I'd like more clarity about white space.

  • f.x means getField @"lbl" f
  • f .x (note the space after f) means.... what? Perhaps f (\r -> r.x)?
  • f (.x) presumalby really does mean f (\r -> r.x)
  • f(.x) presumably means the same thing.

Perhaps the right way to think about it is that .x is a postfix operator. You cannot put any white space after the dot, but you can always put as much as much whitespace before the dot.

GHC already supports postfix operators: here is the manual section. It would be good to check that the proposal is compatible with treating .x as a postfix operator in the sense of that section.

@shayne-fletcher-da shayne-fletcher-da force-pushed the shayne-fletcher-da:record-dot-syntax branch from 816a8b6 to a6c3612 Oct 11, 2019
@shayne-fletcher-da shayne-fletcher-da force-pushed the shayne-fletcher-da:record-dot-syntax branch from a6c3612 to 03ef0a1 Oct 11, 2019
@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

The link is broken in the rendered proposal, where it says "This proposal is discussed at this pull request"

So fast Simon! I was just fixing it 😉

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

You cannot put any white space after the dot, but you can always put as much as much whitespace before the dot.

Yes, that's how it behaves.

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

I'd like more clarity about white space.

  • f.x means getField @"lbl" f

Yes.

  • f .x (note the space after f) means.... what? Perhaps f (\r -> r.x)?

f .x is the same as f.x (that is, getField @"lbl" f).

  • f (.x) presumalby really does mean f (\r -> r.x)

Exactly.

  • f(.x) presumably means the same thing.

Yes.

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Oct 11, 2019

Thanks. Perhaps in due course update the proposal to make these points clear.


Below are some possible variations on this plan, but we advocate the choices made above:

* Should `RecordDotSyntax` imply `NoFieldSelectors`? They are often likely to be used in conjunction, but they aren't inseparable.

This comment has been minimized.

Copy link
@adamgundry

adamgundry Oct 11, 2019

Contributor

I'd argue that it should not. Nothing about this proposal requires that selectors not exist, it is merely helpful to avoid clashes (for which DuplicateRecordFields would work pretty much equivalently).

If a consensus emerges that RecordDotSyntax + NoFieldSelectors + ... is the right way forward, we could easily add a new extension Haskell2030Records that implies the conjunction.

@adamgundry

This comment has been minimized.

Copy link
Contributor

adamgundry commented Oct 11, 2019

In addition to the lack of type-changing updates, the HasField approach is limited compared to existing selector functions in that it cannot support higher-rank fields. (This is arguably more related to the HasField proposal, but perhaps worth flagging up in this context as well.) For example, given

data T = MkT { foo :: forall a . a -> a }

then foo (MkT id) is well-typed but getField @"foo" (MkT id) is not.

Perhaps this use is rare enough that users can re-enable selector function generation (or define their own selectors) in this case.

@phadej

This comment has been minimized.

Copy link
Contributor

phadej commented Oct 11, 2019

{-# LANGUAGE RecordDotSyntax #-}

import qualified Foo

data Foo = Foo

instance HasField "name" Foo () where hasField = ...

something = ... Foo.name

is that Foo.name

  • a name from Foo module
  • or getField @"name" Foo

EDIT:

Foo.name  -- name from module Name
Foo .name -- getField ...

Proposal should point this out.

@nomeata

This comment has been minimized.

Copy link
Contributor

nomeata commented Oct 11, 2019

Perhaps this use is rare enough that users can re-enable selector function generation (or define their own selectors) in this case.

It may be rare, but it turned out to be a blocker for various ways of fixing #216.

@parsonsmatt

This comment has been minimized.

Copy link

parsonsmatt commented Oct 11, 2019

The loss of polymorphic update is a huge problem, imo.

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

Foo.name

This parses as a qualified variable, not field selection.

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Oct 11, 2019

The e{lbl * val} syntax is a bit perplexing to me. Firstly, it has no = sign, making it hard to recognize at first that there's an update going on here. Secondly, it makes commutative operators non-commutative. That is, the following two lines are not equivalent:

  1. c{taken.year + n}
  2. c{n + taken.year}

I would propose that we introduce a e{lbl * = val} syntax instead. The examples from the proposal would look like:

addYears :: Class -> Int -> Class
addYears c n = c{taken.year + = n} -- update via op

squareUnits :: Class -> Class
squareUnits c = c{units & = \x -> x * x} -- update via function

It would also nicely parallel the C++ syntax +=, -=, *=, etc, differing only in whitespace.

@phadej

This comment has been minimized.

Copy link
Contributor

phadej commented Oct 11, 2019

The getField part looks not like PostfixOperators but likeTypeApplications

foo @Int .field1 @Bar .field2

I can imagine having an Overloaded:RecordDots option in overloaded plugin, as val .name could be desugared into whatever @"name" val, not only getLabel

So for me, the getField part of this proposal is simply introducing some new expression syntax. More things to play with.

@phadej

This comment has been minimized.

Copy link
Contributor

phadej commented Oct 11, 2019

In #282 (comment) the

f .x (note the space after f) means.... what? Perhaps f (\r -> r.x)?

f .x is the same as f.x (that is, getField @"lbl" f).

The proposal should somehow specify how juxtaposition works now, it looks like some will be more binding than another:

someFun record .field .field2 value

should probably be parsed the same as

someFun record.field.field2 value

i.e.

(someFun ((record.field).field2)) value

Should or shouldn't be there warnings for omitted front space? It feels like "don't use tabs" thing.

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

Foo.name  -- name from module Name
Foo .name -- getField ...

Proposal should point this out.

I shall take care of it.

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

The proposal should somehow specify how juxtaposition works now, it looks like some will be more binding than another:

someFun record .field .field2 value

should probably be parsed the same as

someFun record.field.field2 value

i.e.

(someFun ((record.field).field2)) value

As it's implemented in the prototype, function application is taking precedence over field projection so f a.foo.bar.baz.quux 12 parses as ((f a).foo.bar.baz.quux) 12. To treat the first argument to f as a projection of a, write f (a.foo.bar.baz.quux) 12 and f (a .foo .bar .baz .quux) 12 is equivalent. Update : #282 (comment)

@phadej

This comment has been minimized.

Copy link
Contributor

phadej commented Oct 11, 2019

As it's implemented in the prototype, function application is taking precedence over field projection

That's definitely should be pointed out in the proposal. It's an opposite of what I thought it is.

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

That's definitely should be pointed out in the proposal.

Maybe best to go to the unresolved questions section. Hard to call the "right" parse here.

@gbaz

This comment has been minimized.

Copy link

gbaz commented Oct 11, 2019

In my opinion, this proposal leaves at least one huge thing to be desired. It proves an "alternate route" to compositional projection (besides generating projection functions directly) by having the . denote a projection function. However it does not provide any route to compositional update. In our experience (at awake), this feature is key. In particular, having sugar for both makes the "stealing" of the syntax from idiomatic lens usage much less painful.

Here's one way to do it. Just as (.lbl) expands to (\x -> x.lbl), have:

(.=lbl) ==> (\x v -> x{lbl = v})

[of course, pick your exact syntax poison of choice -- &= is another good candidate].

One nice thing here is you can even parse the "assignment" as an infix operator if desired, so x .=lbl v reads as x with lbl assigned to value v.

This could also desugar to using SetField if you prefer. However, I confess by the way I don't understand why SetField is used at all in this proposal?

I.e., it has e{lbl = val} ==> setField @"lbl" e val. But why not just leave it as is? The subsequent (nested) desugaring e{lbl1.lbl2 = val} ==> e{lbl1 = (e.lbl1){lbl2 = val} would still work correctly, no? And furthermore, if SetField isn't used, then doesn't polymorphic record update still work?

@simonpj

This comment has been minimized.

Copy link
Contributor

simonpj commented Oct 11, 2019

I shall take care of it.

It'd be great to update the proposal to cover all the syntactic questions here, so that it stands by itself without reading the discussion thread. For example

As it's implemented in the prototype, function application is taking precedence over field projection so f a.foo.bar.baz.quux 12 parses as ((f a).foo.bar.baz.quux) 12. To treat the first argument to f as a projection of a, write f (a.foo.bar.baz.quux) 12 and f (a .foo .bar .baz .quux) 12 is equivalent.

Make sure the proposal says all this!

@shayne-fletcher-da

This comment has been minimized.

Copy link
Contributor Author

shayne-fletcher-da commented Oct 11, 2019

I shall take care of it.

It'd be great to update the proposal to cover all the syntactic questions here, so that it stands by itself without reading the discussion thread. For example

As it's implemented in the prototype, function application is taking precedence over field projection so f a.foo.bar.baz.quux 12 parses as ((f a).foo.bar.baz.quux) 12. To treat the first argument to f as a projection of a, write f (a.foo.bar.baz.quux) 12 and f (a .foo .bar .baz .quux) 12 is equivalent.

Make sure the proposal says all this!

Understood Simon. On it... Done.

@cocreature

This comment has been minimized.

Copy link

cocreature commented Oct 11, 2019

This could also desugar to using SetField if you prefer. However, I confess by the way I don't understand why SetField is used at all in this proposal?

This proposal tries to solve two things at once (which could probably be pointed out more clearly in the proposal):

  1. The use of dot-syntax and together with that an easy way to deal with nested fields. This is independent of SetField.
  2. A better solution to colliding field names than DuplicateRecordFields. This depends on SetField.
@phadej

This comment has been minimized.

Copy link
Contributor

phadej commented Oct 11, 2019

@gbaz, one value of desugaring to a something using class, is that one can write manual instances to the class. I.e. you can do "Classy Lenses" stuff. Compare with writing "foo" with and without {-# language OverloadedStrings #-}. Yet, {-# language OverloadedRecordDots #-] would be too much, i.e.

  • plain {-# language RecordDotSyntax #-} would desugar to selector functors and record updates and
  • {-# language RecordDotSyntax, OverloadedRecordDotSyntax #-} would desugar to a type-class members

It's silly to be so granular here, but OTOH there are various things happening: new syntax, and overloading that syntax with existing HasField functionality.


@cocreature was first, and I agree

This proposal tries to solve two things at once (which could probably be pointed out more clearly in the proposal)

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Nov 13, 2019

Freedom to format code with whitespace

That's just not something you have in Haskell, especially now that https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0229-whitespace-bang-patterns.rst was accepted. We are definitely going down the route of whitespace-aware parsing, and why wouldn't we? Whitespace matters to humans, so the parser might also take the hint.

You cite the precedent with record updates: OK. Then I'm going to cite the precedent with Template Haskell (e.g. f $ x vs f $x). Or the precedent with module qualifications (Just . id vs Just.id). Or the precedent with as-patterns and type-applications (f x@p = ... vs f x @p = ...).

Maybe you want a language that ignores whitespace, but then why come into a Haskell discussion and declare "That's a disagreement and it's completely fine"? If it's fine then let it rest. Haskell gives whitespace meaning, and it's a handy feature.

@chrisdone

This comment has been minimized.

Copy link

chrisdone commented Nov 13, 2019

That's just not something you have in Haskell,

I wrote hindent. This is something you have in Haskell. Maybe you disagree and that's fine. But having written a complete printer for Haskell, I'm very aware of the whitespace sensitivity and it's not all that.

We are definitely going down the route of whitespace-aware parsing,

We already have whitespace-aware parsing.

You cite the precedent with record updates: OK. Then I'm going to cite the precedent with Template Haskell (e.g. f $ x vs f $x). Or the precedent with module qualifications (Just . id vs Just.id). Or the precedent with as-patterns and type-applications (f x@p = ... vs f x @p = ...).

Great. In other words, we both have examples that confirm our belief that a given syntax is perfectly fine and not alien to the language.

Maybe you want a language that ignores whitespace,

I want a language that ignores whitespace where I want it to. I don't know what language you want, nor am I asking for it.

but then why come into a Haskell discussion

As is my freedom to do so. But thanks for making me feel welcome.

and declare "That's a disagreement and it's completely fine"? If it's fine then let it rest.

I am trying to let it rest, but @tysonzero continues, repeateadly, to beat a dead horse. Look at @cdsmith, he's given up and complained repeatedly about having to retread things that have already been discussed.

Haskell gives whitespace meaning, and it's a handy feature.

Insert "sometimes" in there and we agree entirely.

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Nov 13, 2019

As is my freedom to do so. But thanks for making me feel welcome.

I'm just not sure what you're trying to achieve by using this freedom. Disagree with several people and then tell them it's fine, as if they don't have the right to provide a counter-argument? What's the point of participating in an argument if you don't want to reach consensus, which is supposedly the goal of having an argument in the first place?

Let's put it this way:

  1. If you want your opinion to count, then "it's fine to disagree" is not going to work because it's up to you to make others agree with you.
  2. If you don't want your opinion to count, then it's easier not to express it in the first place.

I probably came across unwelcoming by suggesting (2). But (1) is totally fine with me either: please, go ahead, provide more (preferably technical) arguments in favor of your preferred option, but the price is that it's not fine to disagree anymore. The goal of having an argument is to make everyone agree.

Insert "sometimes" in there and we agree entirely.

We still disagree whether this "sometimes" should apply to .lbl. Consider map .lbl [1,2,3]. What makes .lbl more like a record update than an overloaded label? That is, we can draw two comparisons:

  1. map { x = 5 } [1,2,3]
  2. map #lbl [1,2,3]

To me, map .lbl [1,2,3] looks more like (2). In particular, because in both cases we have a symbol (# or .) followed by letters interpreted as a string. Does it look more like (1) to you? If so, why?

@takenobu-hs

This comment has been minimized.

Copy link
Contributor

takenobu-hs commented Nov 13, 2019

To land this proposal, it's better to recognize various mental models.
This proposal might break someone's mental model, because it borrows existing syntax.

For instance, I summarize some mental models on this thread:

White space beside a function : (for instance f .lbl)

interpretation
mental model a1 function application
mental model a2 ignorable delimiter

Naked dot .: (for instance .lbl)

interpretation
mental model b1 function composition; right to left chain
mental model b2 function application; left to right chain
mental model b3 creating field function
: :

Section syntax ( ): (for instance (.lbl))

interpretation
mental model c1 special syntax for creating various function
mental model c2 unsaturated function application
: :

I hope this useful for you.

@Sevensidedmarble

This comment has been minimized.

Copy link

Sevensidedmarble commented Nov 13, 2019

This is a cool argument and all but I don't get how anyone could possibly have a problem with: foo x .y z .y I think 99% of programmers would assume this is an ok way of accessing a record. Unless there's some knock on problem from this I don't understand.

@ocharles

This comment has been minimized.

Copy link

ocharles commented Nov 13, 2019

@Sevensidedmarble I parse that as (foo) (x) (.y) (z) (.y) - that is, a function call to foo with 4 arguments. Is that how you are parsing foo x .y z .y? You might think "99% of programmers would assume this is an ok way of accessing a record", but please spell out what you think 99% of programmers would read.

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Nov 13, 2019

I don't get how anyone could possibly have a problem with: foo x .y z .y I think 99% of programmers would assume this is an ok way of accessing a record. Unless there's some knock on problem from this I don't understand.

I don't have any problems with it either. Clearly, the whitespace denotes function application, so this example should parse as foo applied to four arguments, which are x, .y, z, and .y. That's how we do it with overloaded labels.

foo x #y z #y    -- parsed as (foo) (x) (#y) (z) (#y)
foo x .y z .y    -- parsed as... (you tell me)
@chrisdone

This comment has been minimized.

Copy link

chrisdone commented Nov 13, 2019

@int-index

What's the point of participating in an argument if you don't want to reach consensus, which is supposedly the goal of having an argument in the first place? [..] The goal of having an argument is to make everyone agree.

I think this is a combative/narrow view of discourse. The goal can also be to see exactly where you disagree, as some opinions are going to be based on personal priorities and won't be subject to change. Once you know where you disagree fundamentally, then you can decide whether and where to compromise and to document these compromises. If people refuse to accept that others have different values, instead continuing to try to change their minds, accepting nothing else, it will go round in circles, as it has been doing; exhausting everyone in the process, myself included.

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Nov 13, 2019

Once you know where you disagree fundamentally, then you can decide whether and where to compromise and to document these compromises.

And someone gets to make the final decision anyway. If you don't want to "make everyone agree", then at least the goal is to present your preferred option to those who make the final decisions (GHC Steering Committee). And I suppose that's what you're asking for, judging by #282 (comment) you want your viewpoint to be represented more accurately in the proposal?

If so, then indeed it's a shame that the argument has not been properly incorporated there. But then I don't think venting off at @tysonzero using sarcastic emojis (#282 (comment)) is a helpful strategy towards this goal.

Maybe the most productive approach would be to make a pull request with your edits. Recently, I made a pull request to the "Overloaded Quotation Brackets" proposal and now my rhetoric is properly represented there.

@cdsmith

This comment has been minimized.

Copy link

cdsmith commented Nov 13, 2019

FWIW, I think Chris is spot on here. I'm trying to make allowances for people having different personal norms, but what has been really exhausting here is putting in a lot of effort to clarify the different perspectives, only to come back and see that completely ignored and the conversation continued as if nothing had been said, or worse, misrepresenting or caricaturing the reasons for that disagreement. That might be a way to win internet-arguments, but it is not productive at all for understanding the situation and making a good decision. This refusal to acknowledge other points of view is what is making this conversation drag around in circles.

@ndmitchell

This comment has been minimized.

Copy link
Contributor

ndmitchell commented Nov 13, 2019

FWIW, to me, a "section" is a lambda with a syntactically elided bit. That's why (* x), (,x,) (:: X), (.x), ({x=1}) are all obvious enough sections. It's why I think (.x) is intuitive (easy to guess something is elided, and from the, to guess what is elided), but .x requires learning that this special form means something special.

Thanks for all the work to try and tabulate things - anything we can do to reach a final conclusion is appreciated. All the coauthors of this proposal are currently travelling 🙂. I agree with several things (eg wrapping something in pointless brackets is not a good argument for .x being the right choice) and will update the proposal when I get a chance.

I do think it would be a mistake to not include one of (.foo) or .foo, deferring it to a follow up proposal. The initial implementation at Digital Asset didn't have either, and it was quickly the number one feature request, and made certain uses very difficult without it.

@gridaphobe

This comment has been minimized.

Copy link
Contributor

gridaphobe commented Nov 13, 2019

Maybe the most productive approach would be to make a pull request with your edits. Recently, I made a pull request to the "Overloaded Quotation Brackets" proposal and now my rhetoric is properly represented there.

Yes, I think it would be great if @chrisdone and @cdsmith could submit a PR to improve the proposal's coverage of their stance. It's important not just for future reference but also for the Committee right now. This discussion has gotten extremely long and while some Committee members have been actively participating, it will be difficult for the rest to pore through the entire conversation and retain the salient bits.

I think at this point, where it seems people are mostly restating existing points of disagreement, the most important thing is to ensure that everyone's views are accurately reflected in the proposal text.

@chrisdone

This comment has been minimized.

Copy link

chrisdone commented Nov 13, 2019

I hadn't realized I could submit a PR against the PR! That's a good direction, I'll direct my energy there instead. Thanks for the productive suggestions, everyone.

@cdsmith

This comment has been minimized.

Copy link

cdsmith commented Nov 13, 2019

One pragmatic suggestion here: it would be great if there were also a -Wdot-without-spaces option (I'm definitely not attached to that name), which can be enabled WITHOUT RecordDotSyntax, and would warn about dot as an infix operator without whitespace, where the meaning would change when this extension is enabled.

@int-index

This comment has been minimized.

Copy link
Contributor

int-index commented Nov 13, 2019

@cdsmith

This comment has been minimized.

Copy link

cdsmith commented Nov 13, 2019

@int-index Oh, I can't get warnings about dangerous stuff without also having GHC whine about x^2? That's too bad.

@akhra

This comment has been minimized.

Copy link

akhra commented Nov 13, 2019

I agree that dot may be special enough to deserve its own whitespace warning.

And to reiterate, I agree with @chrisdone 's intuition and preferences here, though I understand the counterarguments about function application and concede Simon's point that other recent/pending extensions may change priorities in ways I haven't thought through.


I also agree with his exhaustion. I watched Chris explain his (and, generally, my) position clearly, and have it apparently ignored in favor of reiterating points he had already acknowledged. I eventually expressed it myself, in different terms and tone than Chris, and received similar responses. Not universally, but enough to make me feel that I'm simply not being listened to by most of the people actively arguing -- at which point, what's the point in speaking further?

Debate can only be productive if both parties assume the other side has good reasons for their position, and make an effort to understand the opposite position. I made sure, before I entered the conversation, that I follow the reasoning and priorities behind the bare .lbl preference (and I don't think it's wrong, just a priority mismatch). This is what "good faith argument" means, and in several instances I've felt it was lacking here.

I think this is a baseline cultural issue, not intentional on anyone's part; but I'm making some noise because improving it must be intentional, or it won't happen. And certainly, plenty of people here are setting good examples too! Notably Simon, as always; and a special thanks to @gridaphobe for finding a constructive direction.

@benjaminbaker

This comment has been minimized.

Copy link

benjaminbaker commented Nov 13, 2019

I have been an observer of this conversation, but have decided that due to the disrespectful nature of some the comments, I wanted to leave my two cents.

@akhra I understand you and @chrisdone are on the same side here but as an observer of this discussion I think the characterization of the other side debating in bad faith is wildly inaccurate, and in fact, I'd argue that this discussion has seen reasonable responses to your comments.

However, the sarcastic emoji usage such as here and comments like this where @chrisdone just repeatedly responded "Don't care" to various points raised, have left a bad taste in my mouth.

I am trying to let it rest, but @tysonzero continues, repeateadly, to beat a dead horse.

@chrisdone you are clearly not putting it to rest, as you have made a separate reddit thread on r/haskell here trying to rally votes for your viewpoint, even though it has already been linked to reddit, voted on in the GitHub poll, and even after the subsequent twitter poll that did not even include your opinion (originally option 3).

If you are wanting to pass option (3), then it should be by the merits of your technical arguments, not rallying reddit haskellers to vote brigade for your opinion after we have already had weeks long discussion on the proposal, and not by being disrespectful.

@akhra

This comment has been minimized.

Copy link

akhra commented Nov 13, 2019

To be clear, while we agree on topical points, I'm not endorsing all of Chris's rhetoric. He has absolutely contributed to the combative mood. Perhaps I have, too. Nobody's perfect; all I'm saying is let's please try, and here's one specific action point.

To this, though:

I'd argue that this discussion has seen reasonable responses to your comments.

I agree 100%, and I tried to acknowledge that above. But I also felt that many were unreasonable, and those have a disproportionate effect on (at least my experience of) the conversation's character. Perhaps more explicit highlighting of good interactions would help, though.

@tysonzero

This comment has been minimized.

Copy link

tysonzero commented Nov 14, 2019

I agree that respect and good faith arguments are important. I do think I have tried to use reasonable and good faith arguments, even though they of course aren’t perfect.

One thing I will add. If we are trying to minimize the number of mistakes that parse and lead to a potentially confusing type errors. You actually end up with less mistakes parsing when you don’t use parenthesis.

-- both parse
foo % (.bar)
foo % (. bar)

-- only one parses
foo % .bar
foo % . bar

So I would honestly say that bare .bar has a decent claim to being more beginner friendly. You teach it just like any other identifier just with a . as the first character, and a good chunk (but not all) spacing mistakes are caught by the parser.

@Tarmean

This comment has been minimized.

Copy link

Tarmean commented Nov 14, 2019

I feel like there are three very different interpretations users new to this feature might have:

  • . as namespace for selectors
  • foo.bar as syntactic element
  • . as a binary accessor operator

The first interpretation makes bare .foo very natural but x.y == .y x still feels unintuitive to me.

The second interpretation makes (.foo) fairly natural since it is reasonably similar to tuple sections.

The third interpretation is just wrong. It would suggest (.foo) which might actually be a good point against using brackets since brackets might incourage this interpretation.

For something completely unrelated, I would prefer a slightly a fancier desugaring to give more options to RebindableSyntax. As in, desugaring x.y into something like viewAccessor .y x and .y.z into composeAccessors .y .z. That ship has most definitely sailed, though.

@int-index int-index referenced this pull request Nov 14, 2019
@cdsmith

This comment has been minimized.

Copy link

cdsmith commented Nov 14, 2019

@Tarmean I definitely think getField is fair game to resolve locally with RebindableSyntax. Perhaps that should be mentioned in the proposal. Is there any advantage to defining a new name instead of just redefining getField?

Composition gets messier, and I'm personally skeptical that it's worth the complexity. When if you desugar .foo.bar to .bar . .foo, I suspect it should be hard-coded to function composition instead of choosing the local ., because it's just not obvious that there's a . in the desugaring. And people do change . in custom preludes, often to something involving Category.

@blamario

This comment has been minimized.

Copy link

blamario commented Nov 14, 2019

Since it's quite clear that there is no consensus on the meaning of .field syntax, the obvious solution is to not give it any by default. Make it a syntax error. Also introduce further language extensions, DotFieldAccessSyntax and DotFieldSectionSyntax that implement the two alternatives on the table, both implying RecordDotSyntax. One of them may prove vastly more popular in time, who knows.

There is a fairly simple story that can explain all this. In Haskell 2010, the . character maps to a single lexeme. With the RecordDotSyntax extension, . maps to one of two lexemes depending on whether it's surrounded by whitespace on both sides or not (on both sides!). Whitespace only on one side is a lexical error. The DotFieldAccessSyntax and DotFieldSectionSyntax keep the same two lexemes but remove the error condition, picking one or the other lexeme instead.

@akhra

This comment has been minimized.

Copy link

akhra commented Nov 14, 2019

  • . as namespace for selectors
  • foo.bar as syntactic element
  • . as a binary accessor operator

The third interpretation is just wrong. It would suggest (.foo) which might actually be a good point against using brackets since brackets might incourage this interpretation.

@Tarmean This is a factor I hadn't considered. Indeed, it seems like the core split between Team .lbl and Team (.lbl) is the first interpretation vs. the second; but pushing intuition toward the third is a danger with real consequences. I've seen the fallout of that sort of misunderstanding.

@tysonzero 's point here is related:

-- both parse
foo % (.bar)
foo % (. bar)

This leaves me closer to neutral. I still aesthetically prefer (.lbl) and f .x == f.x, but I hadn't considered calling . a namespace. From that perspective, bare .lbl as a prefix function feels a lot more natural; something I can easily build (and communicate) a clear intuition around, rather than a weird ad-hoc syntactical rule that has to be remembered on its own terms.

Concretely: changing my vote from (3)>(1)>(2) to (3)>(2)>(1).

@akhra

This comment has been minimized.

Copy link

akhra commented Nov 14, 2019

introduce further language extensions

@blamario I had considered making this suggestion a few days ago, but ultimately I think it's worse than nothing. It creates a hard fork in the language: these aren't just two ways of doing things, they're two completely incompatible ways. If both are released into the wild, we're basically stuck supporting them forever or we run the risk of breaking production code. At best we can eventually deprecate one, but that creates documentation and (ongoing!) pedagogical costs.

How many extensions are already Considered Harmful? How long is the list that a newbie has to wade through, learning which ones to avoid? Splitting the options is guaranteed to add at least one, and I don't think that's okay.

@Tarmean

This comment has been minimized.

Copy link

Tarmean commented Nov 14, 2019

@cdsmith In the current design .x = (\x -> x.lbl). I was thinking that in some use cases standalone .x might be useful as non-function-valued - for lenses or bidirectional transformations or whatever. So being able to use RebindableSyntax to get different semantics for x.y and .y x might be neat. As a silly example:

.lbl => hasField @lbl 
x.lbl => snd .lbl x

I will readily admit that actually doing this would most likely be incredibly janky and complicating the desugaring is almost certainly not worth the complexity cost.

I think I want this because x.y == .y x feels odd to me. Standalone .x being something lens-like so x.y == view .y x would feel more natural. I was half tempted to propose view .x as a third option for standalone accessors that technically would allow backwards compatible extension of RecordDotSyntax to full lenses. However some sliver of remaining sanity intervened.

@blamario That seems like an.. awkward solution. People will get used to whatever the decision ends up being after a while. Having the semantics change depending on the top of the file and the cabal/stack config would make getting used to one solution a lot harder. I also don't see a nice transition to the winning style at a later date.

@tysonzero

This comment has been minimized.

Copy link

tysonzero commented Nov 15, 2019

@Tarmean

I also think such a thing would be quite cool. But we don't need to change the existing proposal at all to support it.

Here is my explanation for an intuitive and backwards compatible way to extend the proposal later to support such a thing. It works basically the same way OverloadedLabels works.

It would give the best of both worlds in that you can still do:

map .name people

foo (.owner.name <$> organizations)

but you would also be able to do:

newPerson = oldPerson & .name .~ "Bob"

query = select . from $ \p -> do
    where_ $ p >- .age >=. 18
    pure $ p >- .name
@Tarmean

This comment has been minimized.

Copy link

Tarmean commented Nov 15, 2019

@tysonzero Pretty sure that class needs functional dependencies to be usable, otherwise .foo.bar would have an ambiguous type in the middle. Which makes sense - currently you could encode .foo in the classy lens style so ghc can't know which .bar we are talking about.

I feel like giving foo.bar a monomorphic type that only depends on foo's type is really important for usability. Maybe that lookup could actually be kind directed? That would make the type class basically unextendable for libraries but it could give decent type inference.

There might be some point in the design space around locally polymorphic functions which allow a closed-world attitude. No idea how that would mix with current haskell, though.

@tysonzero

This comment has been minimized.

Copy link

tysonzero commented Nov 15, 2019

@Tarmean

It does not need functional dependencies to be usable. I'm not sure what kind of functional dependency you would want to add, both x -> a and a -> x would make the class essentially useless.

With the IsField class, the GetField => IsField (->) instance, and the SetField => IsField Setter instance, here is what the inferred types of various expressions would be:

.foo :: IsField "foo" a => a

(\x -> x.foo) :: GetField "foo" r a => r -> a

-- with `.bar . .foo` desugaring
.foo.bar :: (GetField "foo" a b, GetField "bar" b c) => a -> c

-- with `.foo >>> .bar` desugaring
.foo.bar :: (IsLabel "foo" (cat b c), IsLabel "bar" (cat a b), Category cat) => cat a c

-- with either desugaring
(\x -> x.foo.bar) :: (GetField "foo" a b, GetField "bar" b c) => a -> c

.foo .~ 5 :: (Num a, SetField "foo" r a) => r -> r

-- with `.foo >>> .bar` desugaring (doesn't typecheck with `.` desugaring)
.foo.bar .~ 5 :: (Num a, SetField "foo" r b, SetField "bar" b a) => r -> r
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.