-
Notifications
You must be signed in to change notification settings - Fork 10
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
Dart 2.0 Support #24
base: master
Are you sure you want to change the base?
Dart 2.0 Support #24
Conversation
Signed-off-by: devkabiir <[email protected]>
Signed-off-by: devkabiir <[email protected]>
This adds the needed type parameters missing in `_run`
Signed-off-by: devkabiir <[email protected]>
…uctors Static methods can have generic types, which allows for more specificity
This reverts commit 8a2ea17. Which was accidently pushed to master
Thanks for this great contribution! I'm wondering: which version of dart are you using? With v2.3.0.dev.0.1 I'm getting an error when running the full_arith example (which I understand you have fixed):
|
@polux I'm using Dart stable 2.2.0 |
Looks like |
The good news is the tests pass on 2.3. Would you mind fixing the full_arith example before I merge? Also I'm getting errors when running |
const Undefined(); | ||
} // simulates the old ? operator | ||
/// Helper to cast [original] to [C] | ||
C cast<C>(Object original) { |
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 still trying to digest all the changes but I fail to understand the need for this helper method: why do we need special cases for String, List, num, int and double? Why isn't return orginal as C
enough?
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.
Because of nested types. The compiler was not accepting say Parser<List<dynamic>> as Parser<List<String>>
, even Parser<dynamic> as Parser<String>
doesn't work, when I started working on it. But since then I've added a lot types. Few places might still have dynamic
for input or output. Plus this method also helped migrate our parser with little efforts.
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.
At the time thought I needed it for all dart primitives. After testing locally I seems we don't need it for any primitives now. Only a null
check and original as C
;
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.
Thanks! Why do we need the null check for?
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.
Because of failed parses. They return a ParseResult.failure()
which doesn't have a type by it's nature. But I added a generic type to it too and made it a static method so it can have generic types. Now if we don;t specify the type while creating a failure ParseResult
it will become ParseResult<dynamic>
and when dynamic is introduced the type system goes nuts. So if C is Null
in case of a Parser<Null>
or if original == null
in case of a null result. We don;t care about the types we just return null. Also null can't be type casted to anything else. But null
can take place of anything else. null as String
will always have type Null
so if a thing that called cast<String>(something)
was expecting a value of type String
but got a value of type Null
instead it will freak out at runtime. But that thing will happily accept a String of value null
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 hope I didn't make it even more confusing.
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.
Ok, I see what's happening (at least in some cases). In copy
I'm using a null value
as a way to mark it wasn't set by the caller. But in some cases (like in skipManyUntil
) I genuinely want the value to be null
because the parser is of type Parser<Null>
. Yet in that case this line:
cast<B>(value ?? this.value),
evaluates to:
cast<B>(this.value)
which is not the intended behavior. This causes errors like type 'String' is not a subtype of type 'Null' in type cast
. The remedy is to have a default value other than null
for the value
parameter. Some singleton value (or an "Undefined" class for instance) that we can quickly check against using pointer equality.
I'm happy to merge your PR and work from there. But if you prefer we can also continue this code review and you can do the changes.
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.
Ahah, the singleton trick won't work because it can't be of type B. I guess it's just easier to have two copy methods: one for when there's a new value and one for when there isn't.
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 see, I tried the exact same approach in my decorator package in copyWith method. But switched to using a bool flag for being explicit that the caller wants it to be null
. We could do the same here?
So it would become nullValue ? null : value ?? this.value
.
Edit: And the caller would do copy(....value: myValue, nullValue: mayValue == null)
. In case they aren't sure it's gonna be null or not. I know this increases the efforts, but solves the type issues and help being more explicit which in turn helps the next programmer.
I don't know how to fix it without running About the analyzer. I ran it locally for individual files. And on directories too. it worked fine. Except for a deprecation warning on a test. The analyzer will ignore any files present in the |
Parser<B> or<B extends A>(Parser<B> p) { | ||
return new Parser<B>((s, pos) { | ||
ParseResult<B> res = _run(s, pos); | ||
// refact(devkabiir): Since B extends A, A can always be used in place of B, not the other way around |
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 is the other way around. Silly me. Can you look at or
method? and the |
operator. The way it is now 'works', But I wasn't sure a the time if this refactoring was required. Say
Parser<B> or<B extends A>(Parser<B> otherParser)
might be intended. But in practical scenario otherParser
don't always extend the original one.
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 static type returned by a union of two parsers should be their most common ancestor. So it should be something like:
Parser<C> or<A extends C, B extends C>(Parser<B> p)
Which can probably be simplified into:
Parser<B> or<A extends B>(Parser<B> p)
since generics are covariant in dart. (At least they were in 1.0, maybe this has been fixed in 2.0? In doubt I wouldn't use the simplified version.)
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.
What do you think about letting the caller decide? The caller would always know better, no? This opens up the possibility of otherParser
not being of same base type in which case or
always returns Parser<dynamic>
.
Parser<O> or<O>(Parser otherParser)
If they don't specify its dynamic
by default.
If they do it gets casted.
e.g.
final c = char('a').or<String>(char('b'));
/// or
final Parser<String> c = char('a').or(char('b'));
print(c.runtimeType); /// will print `Parser<String>`
print(c is Parser<String>); /// true
print(c as Parser<String>); /// works
Another example would be
Parser<RegEx> myRegExParser;
final c = char('a').or<Pattern>(myRegExParser);
/// c is of base type `Pattern`
In case of types with no common base type
Parser<ClassA> intParser;/// parses `'1'` to `1`
Parser<ClassB> stringParser; /// parser `'one'` to `1`
final c = stringParser.or(intParser);
/// Here [c]'s result will always be an `int`
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 not sure I follow: wouldn't the caller also decide with Parser<C> or<A extends C, B extends C>(Parser<B> p)
? The caller could chose to set C
to dynamic
if that's truly the closest common supertype.
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.
Umm, When you say A extends C
which A
are you referring to? In this
Parser<C> or<A extends C, B extends C, C>(Parser<B> otherParser)
A
is not equal to the original A
used when declaring class Parser<A> {...}
. Unless I'm mistaken. Also you'd have to specify all three A,B,C
types when calling or
if a typed parser is desired. Otherwise omit all three and get Parser<dynamic>
. Also I don't think C
can be set to dynamic
since nothing can extend dynamic
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.
In
Parser<C> or<A extends C, B extends C, C>(Parser<B> otherParser)
theA
is a newA
. That's my understanding.
You're correct. What I was aiming at was:
Parser<C> or<B extends C, C super A>(Parser<B> otherParser)
But that doesn't seem to exist. The spec says:
Dart does not support lower bounds for type parameters".
Too bad! (I found a rendered version of the spec by the way: https://www.dartlang.org/guides/language/specifications/DartLangSpec-v2.2.pdf).
Anyway, the spec also says:
17 Class Covariance: A parameterized type based on a generic class C is a subtype of a parameterized type based on the same class C if each actual type argument of the former is a subtype of the corresponding actual type argument of the latter.
So generics are still covariant in Dart 2.0. In the case of parsers I think this is sound: they actually are covariant. So we could use this signature:
Parser<B> or<B super A>(Parser<B> p)
Unfortunately as we'be established lower bounds don't exist in Dart. So I guess we're left with:
Parser<A> or(Parser<A> p)
Let's say S
is a subtype of T
. Then by covariance Parser<S>
is a subtype of Parser<T>
. In p1.or(p2)
if p1
is of type Parser<T>
and p2
is of type Parser<S>
then the typechecker wouldn't complain since p2
is also of type Parser<T>
by subtyping. If p1
is of type Parser<S>
and p2
is of type Parser<T>
then the caller would have to explicitly cast p1
:
(p1 as Parser<T>).or(p2)
Given that in most cases the types should be the same that seems ok to me. What do you think? If you still prefer the "let the user chose" option could you please elaborate on what the signature would look like and where the cast would happen?
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.
17 Class Covariance: A parameterized type based on a generic class C is a subtype of a parameterized type based on the same class C if each actual type argument of the former is a subtype of the corresponding actual type argument of the latter.
I don't fully understand this 😅.
Given that in most cases the types should be the same that seems ok to me. What do you think?
In case they aren't castable to one another there isn't much dart can do anyway, other than Parser<dynamic>
. This is a good approach.
Parser<A> or(Parser<A> p)
This is acceptable. And a win for the |
operator since it removes the generic type from or
.
If you still prefer the "let the user chose" option could you please elaborate on what the signature would look like and where the cast would happen?
Like so, in this case we don't care for the specificity of otherParser
. This gives choice to the caller. If they provide O
then it gets casted. And Dart's type system can also
infer it from somewhere else so the caller doesn't always have to provide the type.
Parser<O> or<O>(Parser otherParser) {
ParseResult<O> parserFunction(String s, Position pos) {
final res = run(s, pos);
if (res.isSuccess || res.isCommitted) {
return res.copy<O>(value: cast<O>(res.value));
} else {
final res2 = p.run(s, pos);
return res2.copy<O>(
expectations: res.expectations.best(res2.expectations));
}
}
return Parser<O>(parserFunction);
}
Unfortunately the |
would then always return Parser<dynamic>
. Which is why I would go for Parser<A> or(Parser<A> p)
but not sure how that would play with current usage of or
throughout the lib
, example
, test
, etc.
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 fully understand this .
It's just a fancy way of saying that List is considered a subtype of List by Dart. It's unfortunately sometimes unsound but that's something dart decided to live with (and rely on runtime checks instead). For example the following program typechecks and fails at runtime:
class Fruit {}
class Apple extends Fruit {}
class Banana extends Fruit {}
main() {
List<Apple> apples = new List<Apple>();
List<Fruit> fruits = apples;
fruits.add(new Banana());
}
Fortunately for us in the case of parsers it is correct that a Parser is a Parser because parsers are "read only" unlike lists.
This is acceptable. And a win for the | operator since it removes the generic type from or.
Good point about the |
operator.
Like so [...]
I see. I generally would like to avoid casts in hot spots of the library. Especially if these casts inspect the generic argument of a function. I don't have proof but I'm pretty certain these types of casts are slower.
but not sure how that would play with current usage of or throughout the lib, example, test, etc.
I guess we need to test 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.
Upon further thinking
Parser or(Parser p)
In this case p
would have to be a covariant of A
, right?
Since there is no generic type parameter, How is the caller supposed use or
when p
is not a covariant of A
?
In case of and
we return ParserAccumulatorN
. How about doing something similar to or
?
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.
@polux
I'm curious to know your thoughts on https://medium.com/dartlang/dart-declaration-site-variance-5c0e9c5f18a5
Could this be used here?
Hey @polux, @devkabiir what's the state of this? |
It's been a while since I last worked on it, If @polux is available for review, I can give it another run. |
Hi, by all means, please give it another run. I'll do my best to spend some time reviewing it. |
Hi @polux, All test are passing.
I know these are a lot of changes to review, so I suggest going commit by commit. I've tried to keep them smaller and stick to what the message says.
I've raised the sdk constraints to
>=2.0.0
Not sure if anyone needs it to be lower than that. Also didn't have time to test it against Dart 1.But it should work!I removednew
andconst
operators, so it won'tI fixed 1 example too
full_arith
.Closes #23