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

Make 1 the default: rename Foo1 things to Foo, and the associated Foo to Foo0 #116

Merged
merged 41 commits into from
Dec 21, 2020

Conversation

martijnhoekstra
Copy link
Contributor

@martijnhoekstra martijnhoekstra commented Dec 14, 2020

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 removed parse and parseAll from Parser0: 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 be Parser0's.

Todo: Fixup the fallout of submitting too early while by while writing up the body of the PR and still running tests.

@martijnhoekstra martijnhoekstra changed the title Make 1 Make 1 the default: rename Foo1 things to Foo, and the associated Foo to Foo0 Dec 14, 2020
@codecov-io
Copy link

codecov-io commented Dec 14, 2020

Codecov Report

Merging #116 (0eae46e) into main (3e79914) will decrease coverage by 0.27%.
The diff coverage is 96.11%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
...shared/src/main/scala/cats/parse/Accumulator.scala 80.00% <85.71%> (-2.86%) ⬇️
core/shared/src/main/scala/cats/parse/SemVer.scala 91.66% <95.45%> (ø)
core/shared/src/main/scala/cats/parse/Parser.scala 94.88% <96.38%> (-0.18%) ⬇️
...ore/shared/src/main/scala/cats/parse/Numbers.scala 100.00% <100.00%> (ø)
...ore/shared/src/main/scala/cats/parse/Rfc5234.scala 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3e79914...0eae46e. Read the comment docs.

Copy link
Collaborator

@johnynek johnynek left a 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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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?

Copy link
Contributor Author

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.

core/shared/src/main/scala/cats/parse/Parser.scala Outdated Show resolved Hide resolved
core/shared/src/test/scala/cats/parse/Rfc5234Test.scala Outdated Show resolved Hide resolved
@rossabaker
Copy link
Member

The MiMa failures in the build would go away with a baseVersion := "0.3".

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.

Copy link
Collaborator

@johnynek johnynek left a 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] =
Copy link
Collaborator

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.

Copy link
Contributor Author

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
Copy link
Collaborator

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?

core/shared/src/main/scala/cats/parse/Parser.scala Outdated Show resolved Hide resolved
core/shared/src/main/scala/cats/parse/Parser.scala Outdated Show resolved Hide resolved
core/shared/src/main/scala/cats/parse/Parser.scala Outdated Show resolved Hide resolved
core/shared/src/main/scala/cats/parse/Parser.scala Outdated Show resolved Hide resolved
@@ -2233,67 +2236,67 @@ object Parser extends ParserInstances {
}

abstract class ParserInstances {
Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

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.

@johnynek
Copy link
Collaborator

Is this ready to merge you think?

@martijnhoekstra
Copy link
Contributor Author

martijnhoekstra commented Dec 19, 2020

flatMap and (soft)product haven't been renamed back to the 10 versions yet (unless you changed your mind about that) and I haven't looked into unifying rep and repAs yet. A RepParser[A] extends Parser[Seq[A]] with methods to set sep, accumulator, min and max also sounds nice, with the current methods as shortcuts creating them. But it's orthogonal to this change I 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] =
Copy link
Collaborator

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?

Copy link
Contributor Author

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

Copy link
Collaborator

@johnynek johnynek left a 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:

  1. Almost all parsers (basically except for whitespace) parse at least one character.
  2. This makes doing the more constrained and safer thing easier and adds a tax to putting less constraint, which is the right bias IMO.

@martijnhoekstra
Copy link
Contributor Author

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.

@johnynek
Copy link
Collaborator

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.

@fthomas
Copy link
Member

fthomas commented Dec 20, 2020

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

@johnynek
Copy link
Collaborator

@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.

@martijnhoekstra
Copy link
Contributor Author

martijnhoekstra commented Dec 20, 2020

@johnynek there is nothing to merge from main -- I rebased earlier today.

@johnynek
Copy link
Collaborator

Without any objections I'll merge in the morning.

Copy link
Collaborator

@regadas regadas left a 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.

@johnynek johnynek merged commit ef9c471 into typelevel:main Dec 21, 2020
@johnynek
Copy link
Collaborator

Thanks all! I think this is a great change to make the library easier to use and steer us more towards non-empty parsers.

This was referenced Dec 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants