petitparser / dart-petitparser

Dynamic parser combinators in Dart.
https://pub.dartlang.org/packages/petitparser
MIT License
453 stars 47 forks source link

Stricter Types #100

Closed stargazing-dino closed 3 years ago

stargazing-dino commented 3 years ago

Hi ! I've noticed that I often lose my types by performing operations like $, | and so on

Turning this on

analyzer:
  strong-mode:
    implicit-casts: false
    implicit-dynamic: false

and it's even more apparent. Does the typing have to be loose for a reason? I'm guessing it does primarily for the fact that the return type is a List for these ops and you can't specify it a type T unless you require both arguments be type T -- but that would likely break the genericness of it.

Parser<String> get openCurly => char('{');
Parser<String> get closeCurly => char('}');

ChoiceParser<String> get curly => openCurly | closeCurly;
// Linter Error 
// A value of type 'Parser<dynamic>' can't be returned from the function 'curly' because it has a return type of 'ChoiceParser<String>'.

I was thinking of a way someone could get around this and thought of something along the lines of what ProxyProvider does from the Provider library. It has implementations of the class 1-6 that I think are generated through build_runner. That is to say, ProxyProvider2... ProxyProviderN. Tuple also has a similar thing through Tuple2-Tuple7. This allows them to take up to 7 types arguments and still correctly pass through the types of each.

For example here is a rough thought of what I had in mind for ChoiceParser

extension ChoiceParserExtension on Parser {
  Parser or(Parser other) {
    final self = this;

    if (self is ChoiceParser2) {
      return ChoiceParser3(
        valOne: self.valOne,
        valTwo: self.valTwo,
        valThree: other,
      );
    } else if (self is ChoiceParser3) {
      // return ChoiceParser4
      throw UnimplementedError();
    } else {
      return ChoiceParser2(valOne: this, valTwo: other);
    }
  }

  Parser operator |(Parser other) => or(other);
}

// I don't know what type Parser would be
class ChoiceParser2<T, R> extends Parser<T> {
  final T valOne;
  final R valTwo;

  ChoiceParser2({
    required this.valOne,
    required this.valTwo,
  });
}

class ChoiceParser3<T, R, S> extends Parser<T> {
  final T valOne;
  final R valTwo;
  final S valThree;

  ChoiceParser3({
    required this.valOne,
    required this.valTwo,
    required this.valThree,
  });
}

Honestly, this solution isn't that clean and would likely require a lot of work so this is more of a stub issue I'm fine with being immediately closed. One thing that would be neat though is if we ever get multiple return values from dart then I imagine this API could be made a lot more type safe relatively easy.

Thanks for the library!

renggli commented 3 years ago

I am aware of the problem. I have experimented with different alternatives in the past, including generating 'arbitrary' sized typed sequences and choices. Unfortunately I haven't found a satisfying solution that is simple, extensible, type safe, and performant.

For choice and sequence parsers what works in my opinion the best is to use literal collections, i.e.

final choice = [a, b, c].toChoiceParser();

Sometimes you might want to enforce a specific type T by adding that:

final choice = <T>[a, b, c].toChoiceParser();

If tuples were supported by the core framework, or support for macros, union and intersection types was added to the Dart language I would be happy to take advantage of that in the library.

As a last resort, there is also parser.cast<T>() and parser.castList<T>().

stargazing-dino commented 3 years ago

Thanks for the advice!

As there is nothing actionable though, I'll close for now. If we ever do get those language features though I'll be sure to reopen. If you prefer to leave open that's fine too. Have a good day