-
Notifications
You must be signed in to change notification settings - Fork 50
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
Make 1 the default: rename Foo1 things to Foo, and the associated Foo to Foo0 #116
Conversation
Codecov Report
@@ Coverage Diff @@
## main #116 +/- ##
==========================================
- Coverage 94.59% 94.31% -0.28%
==========================================
Files 6 6
Lines 758 756 -2
Branches 77 79 +2
==========================================
- Hits 717 713 -4
- Misses 41 43 +2
Continue to review full report at Codecov.
|
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.
A few comments...
I'm still digesting how big the change is. It seems like almost every line touching the API will change.
I'd love to get unanimity from all reviewers this is worth it (@rossabaker @mpilquist @regadas )
between(b, b) | ||
|
||
/** Internal (mutable) parsing method. | ||
* | ||
* This method should only be called internally by parser instances. | ||
*/ | ||
protected def parseMut(state: Parser.Impl.State): A | ||
private[parse] def parseMut(state: Parser.Impl.State): A |
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.
why does this need to be private[parse]
, this does weaken the encapsulation a bit.
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.
Yes, I still want to try harder to narrow access back to the minimum. This was the easy way out to have a compiling PR showing the external API.
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.
If we change this back to protected
it still works, right?
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.
It's now accessed from object Parser, which is a companion of a subclass. Can you do that? IMO spec says yes, compiler says no. scala/bug#2439 says working as intended.
The MiMa failures in the build would go away with a What signfiicant adopters are we aware of? Bosatsu, http4s, anything else? Tweet and gitter about it and invite howls of pain? I agree taking backward compatibility seriously, but I think we take it seriously by finding an API we're happy to live with long into the future than by living with something suboptimal to avoid a scalafix/sed for a few early adopters. Judging it from the example parsers in the repo, I see more suffixes removed than added, and it feels better me. |
18438e0
to
6d85cc6
Compare
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 pretty convinced this is the right move. One thing here is that the safer option: parsers that consume at least 1 thing are the easier to write. That is as it should be. It shouldn't feel like a tax to write a better parser. The tax should come when using the weaker parser (something that might return nothing).
* Without using `with1`, these methods will return `Parser` values | ||
* since they are not known to return `Parser1` values instead. | ||
* Without using `with1`, these methods will return `Parser0` values | ||
* since they are not known to return `Parser` values instead. | ||
*/ | ||
def with1: Parser.With1[A] = |
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.
this is maybe the only 1 name left... I guess it is better than withNot0 or something, but still it feels like it sticks out. I guess we should keep it to minimize change.
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.
along with flatMap01
(because there also is flatMap (which was flatMap10) and flatMap0 (which was flatMap), tailRecM1
which I just didn't see right away and should be changed for consistency, even though it's barely used in user code.
between(b, b) | ||
|
||
/** Internal (mutable) parsing method. | ||
* | ||
* This method should only be called internally by parser instances. | ||
*/ | ||
protected def parseMut(state: Parser.Impl.State): A | ||
private[parse] def parseMut(state: Parser.Impl.State): A |
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.
If we change this back to protected
it still works, right?
@@ -2233,67 +2236,67 @@ object Parser extends ParserInstances { | |||
} | |||
|
|||
abstract class ParserInstances { |
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.
since there are two companions now, is there any reason not to just make this object Parser0
?
Also, should we alias all the def foo0 methods in here so people can do P0.string or P.string rather than P.string0 which is a bit weird.
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.
For the first, I'll try it out.
For the second, I don't have a strong opinion either way. Having a P0.foo
as an alias for P.foo0
meaning something subtly but breakingly different from P0.foo
is a bit of a foot gun. I'm not sure if the added convenience is worth it.
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 follow how they would be different. Can you explain? I am proposing they point to exactly the same code.
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 meant to say the difference between P.foo and P0.foo being the foot gun, but in style I mistyped it.
Is this ready to merge you think? |
I want to check if the scaladoc is all still ok, or now refers to methods that are wrong or no longer exist, and the below comment on orElse is still unresolved. |
@@ -174,8 +174,8 @@ sealed abstract class Parser[+A] { | |||
* one to pick up after any error, resetting any state that was | |||
* modified by the left parser. | |||
*/ | |||
def orElse[A1 >: A](that: Parser[A1]): Parser[A1] = | |||
Parser.oneOf(this :: that :: Nil) | |||
def orElse0[A1 >: A](that: Parser0[A1]): Parser0[A1] = |
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 was just thinking... could we rename this to just orElse
? wdyt?
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.
The result will be that Parser
has this inherited orElse
, and an overloaded orElse
with Parser
as an argument and returning a Parser
. After making the change and search-and-replacing orElse0 with orElse, all the tests still pass.
So I think it's safe to say yes, we could.
I have no strong opinion either way on whether we should and whether we should in this particular PR.
The tension is between the convenience of just being able to say .orElse
and getting the most specific result possible depending on the receiver and argument types and the obviousness of which method is called when one has the 0.
cc @johnynek
orElse -> orElse0, orElse1 -> orElse
Note the typeclass version. I think this is OK, but without looking very closely
the easiest way possible: by loosening access. I tried a bit to do it nicer, but not particularly hard
7a2841a
to
705bb77
Compare
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.
This looks great. I'm inclined to merge now. And strong objections?
To me, the main arguments are:
- Almost all parsers (basically except for whitespace) parse at least one character.
- This makes doing the more constrained and safer thing easier and adds a tax to putting less constraint, which is the right bias IMO.
I have a WIP for a scalafix too: https://github.com/martijnhoekstra/catsparsescalafix It's not up to date for the latest changes yet, doesn't replace some instance methods yet (for some reason I haven't figured out yet), and sometimes leaves unused imports and sometimes clashes imports, but it greatly helps in migrating: https://github.com/johnynek/bosatsu/compare/master...martijnhoekstra:fixedCompile?expand=1 was just a few manual edits from the scalafix. |
Can you add an MD file on how to use it to migrate. It would be awesome if we could figure out how to tell @scala-steward to use that scala fix to upgrade people too. |
Documentation for that is here: https://github.com/scala-steward-org/scala-steward/blob/master/docs/scalafix-migrations.md#adding-migration-rules-to-scala-steward |
@martijnhoekstra can you merge main back in and I won't add anything else until this PR lands. Then it looks like we can send a PR to scalasteward to point to the scalafix you have which hopefully we can get good enough that it is a very minor edit needed for any users. Thanks a ton for this. It was a great idea and a great cleanup. |
@johnynek there is nothing to merge from main -- I rebased earlier today. |
Without any objections I'll merge in the morning. |
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.
LGTM, no strong objections on my side. I would like to revisit the orElse
subject though.
Thanks all! I think this is a great change to make the library easier to use and steer us more towards non-empty parsers. |
To illustrate #113
Moved the constructors and their helper wrappers to
object Parser
(in the easiest way possible, by loosening access), in the second to last commit, and taking things to their logical extreme in I removedparse
andparseAll
fromParser0
: it makes little sense to run an end-parser on a string that may not consume any characters,That the intuition works out is shown in that none of the "real world" tests needed changing other than catching that some
Parser(1)
's could beParser0
's.Todo: Fixup the fallout of submitting too early while by while writing up the body of the PR and still running tests.