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

Unpack macro #7

Open
ReneSac opened this issue Apr 4, 2015 · 12 comments
Open

Unpack macro #7

ReneSac opened this issue Apr 4, 2015 · 12 comments

Comments

@ReneSac
Copy link

ReneSac commented Apr 4, 2015

The result of the docopt call seems a bit unwieldy to use. What about a macro to unpack the table into pretty and readly usable variables, kinda like commandeer?

unpack args:
  variable_name, type, "table-key"

It would be good if the table key can optionally be omitted if the variable name unambiguously refer to one of the table keys. From the front page example:

import docopt

let args = docopt(doc, version = "Naval Fate 2.0")

unpack args:
  name, seq[string], "<name>"  # table-key unnecessary
  x, float
  y, float
  speed, float, "--speed"  # other unnecessary example
  morred, bool
  mineSet, bool, "set"  # a classical case where table-key is necessary

ships[name[0]].move(x, y, speed=speed)

I think it is much better if the typing info is centralized like that, instead of scattered along the code with parseFloat() and such.

The syntax is debatable. And it might be better to make an alternative docopt call to do that directly, w/o the need to create an args table. But of course, one should retain the old way for backwards compatibility/whoever likes it.

@ReneSac
Copy link
Author

ReneSac commented Apr 4, 2015

Rust does this in a different way: https://github.com/docopt/docopt.rs

Seems like there are many different ways to get types out of it. From what I understood, either you create an object with some specific variable names for it to populate, or you specify the types in the call, or you use get_{bool,count,str,vec} on it. It was confusing, but seems more convenient on that than the current Nim's way.

@BurntSushi
Copy link
Member

@ReneSac There are two ways. I don't know of anyone using the get_{bool,count,str,vec} API. It's much less convenient than type based decoding. (Frankly, I should either remove it or discourage its use.) The only other way is to use decoding. This means that you define a struct with your desired types and ask Docopt to decode argv to it. (There is a docopt! macro that creates this struct for you from the usage description. Example: https://github.com/docopt/docopt.rs/blob/master/docopt_macros/examples/macro.rs)

@ReneSac
Copy link
Author

ReneSac commented Apr 4, 2015

Thanks for the input. Reading again now, I think I understood. I still have some questions:

How rust handles arguments --separated-by-dashes? And how it would differentiate it from --separated_by_dashes (actually by underscores, but same otherwise)? Is it important to differentiate between FILE and <file>? Because the rust way won't work for Nim. Also, while you can use the underscore style of variables in Nim w/o problem, the style guide unfortunately claims for camelCase variables, and rust name convention is a bit strange then. But that is not really a problem.

The good thing about using a naming convention is the standardization and simplicity. The bad thing is the lack of flexibility compared to my proposal, and the fact it must be namespaced. Both things are kinda mutually exclusive.

One thing I didn't like about returning a struct is that you need an additional docopt!() outside your main(), because it creates a new type. That is ugly and redundant. Fully defining the struct type must also be done at the top level, but it isn't ugly and the redundancy comes in form of explicitly listing all the elements, which is good for readability.

@BurntSushi
Copy link
Member

How rust handles arguments --separated-by-dashes? And how it would differentiate it from --separated_by_dashes (actually by underscores, but same otherwise)?

It doesn't differentiate them. Rust identifiers cannot contain -, so it replaces - with _. It's a Worse Is Better solution.

Is it important to differentiate between FILE and ?

I think this would actually translate to two different field members, arg_FILE and arg_file, but that seems like an insane edge case to try and correct for.

The good thing about using a naming convention is the standardization and simplicity. The bad thing is the lack of flexibility compared to my proposal, and the fact it must be namespaced. Both things are kinda mutually exclusive.

Sure. I'm just sharing. I have no opinions. :-)

One thing I didn't like about returning a struct is that you need an additional docopt!() outside your main(), because it creates a new type. That is ugly and redundant. Fully defining the struct type must also be done at the top level, but it isn't ugly and the redundancy comes in form of explicitly listing all the elements, which is good for readability.

I don't know what you're talking about. The docopt! macro removes all redundancy. Look:

#![feature(plugin)]
#![plugin(docopt_macros)]

extern crate docopt;
extern crate rustc_serialize;

docopt!(Args derive Debug, "Usage: prog <a> <b> <c>");

fn main() {
    let args: Args = Args::docopt().decode().unwrap_or_else(|e| e.exit());
    println!("{:?}", args);
}

Output:

[andrew@Liger play] cargo run x y z
   Compiling play v0.0.1 (file:///tmp/play)
     Running `target/debug/play x y z`
Args { arg_c: "z", arg_b: "y", arg_a: "x" }

@ReneSac
Copy link
Author

ReneSac commented Apr 4, 2015

It doesn't differentiate them. Rust identifiers cannot contain -, so it replaces - with _. It's a Worse Is Better solution.

Yeah, that is what I expected. Nim identifiers can't contain - either. I agree that a command line interface that depends on this shouldn't be made.

I think this would actually translate to two different field members, arg_FILE and arg_file, but that seems like an insane edge case to try and correct for.

Ok, just wanted to know if it was required per docopt specification/tests.

I don't know what you're talking about. The docopt! macro removes all redundancy. Look:

A non-redundant version in my eyes would be something like the following, that ideally would behave exactly like yours did.:

#![feature(plugin)]
#![plugin(docopt_macros)]

extern crate docopt;
extern crate rustc_serialize;

fn main() {
    let args = docopt!(Args derive Debug, "Usage: prog <a> <b> <c>");
    println!("{:?}", args);
}

But as it must also create a new type I imagine it won't work like that.

@BurntSushi
Copy link
Member

Indeed not. And I'm not sure what redundancy you removed other than writing Args one more time. Getting access to the Docopt builder is important for configuration purposes, like setting the argv or other options. I suppose they could be built into the macro, but probably not worth it.

Anyway, good luck! Let me know if you have any other questions on how it works.

@oprypin
Copy link
Member

oprypin commented Apr 4, 2015

A lot of awesome things could be done if I could run this at compile time. But there are a few reasons why it is impossible: VM can't do ref types and regex.

ReneSac's proposal is something I haven't thought about. It is probably doable without compile-time information, but we would need to define conversions for every pair of source and destination types. I don't like the hardcoded conversions. Users can't choose their own type or conversion. And, finally, what if an argument can have different types depending on context?

@ReneSac
Copy link
Author

ReneSac commented Apr 4, 2015

VM can't do ref types and regex.

Why would we need ref types? Outputting a value type object or a tuple is plenty for this use case.

but we would need to define conversions for every pair of source and destination types

I didn't consider that there were that many source types. Well, you only need to support a few destination types by default, and you don't need to implement all possible combinations between them. Just give an appropriate compilation error if the user asks to convert from seq[string] to float, for example. The user will then have to output it as some supported type and handle it the way he wants latter. More combinations can be added to the program with time. But yeah, I'm not sure how the rust implementation deals with this combinatorial explosion problem.

I don't like the hardcoded conversions. Users can't choose their own type or conversion.

Let the user define their own docopt_decode(v: Value): CustomType procs (or something like that) and use them if provided. For custom types like enums, require them.

And, finally, what if an argument can have different types depending on context?

Output it as a Value and handle that separately. Or redesign your command line interface.

@BurntSushi
Copy link
Member

But yeah, I'm not sure how the rust implementation deals with this combinatorial explosion problem.

It uses the type based decoding infrastructure: https://github.com/docopt/docopt.rs/blob/master/src/dopt.rs#L695-L857 --- The user states the types they want and the decoder tries to convert them. If you have a custom type, then you have to teach it how to decode to your type.

@ReneSac ReneSac closed this as completed Apr 4, 2015
@ReneSac ReneSac reopened this Apr 4, 2015
@ReneSac
Copy link
Author

ReneSac commented Apr 4, 2015

Sorry, misclicked on close.

Bellow an update to my proposal syntax. The default type declaration syntax is sure nicer than the simple comma separated commandeer syntax. There is no need for an intermediate table, all can be done at compile time. The values of course will only be known at run time. Also, the rust prefix syntax can be used as a type of disambiguation.

const doc = """
Naval Fate.

Usage:
  naval_fate ship new <name>...
  naval_fate ship <name> move <x> <y> [--speed=<kn>]
  naval_fate ship shoot <x> <y>
  naval_fate mine (set|remove) <x> <y> [--moored | --drifting]
  naval_fate (-h | --help)
  naval_fate --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.
"""

import docopt, naval_fate_stuff

proc main() =
  docopt(doc, version = "Naval Fate 2.0"):
    drifting, moored, mine, cmd_move, cmd_new, remove, cmd_set, ship, shoot: bool
    speed, x, y: float
    name: seq[string]

  if cmd_move and name in ships:
    echo "Moving ship $# to ($#, $#) at $# kn".format(name, x, y, speed)
    ships[name].move(x, y, speed=speed)

By default it should require you to list all the variables with the type you want, except options that docopt would automatically exit uppon receiving like --version and --help. Safety by default. An optional feature could be enabled to fill in the missing ones.

You may also notice I dropped disambiguation by "". But allowing optional rust prefixes creates new ambiguities, like if you have options named "x" and "flagx" in the same command line. One could simply disallow such clashes, allow only prefixed variables like rust, or reintroduce the "" disambiguation someway.

An alternative version bellow showcasing the above paragraphs:

import docopt

proc main() =
  docopt(doc, version = "Naval Fate 2.0", autoFill=true):
    mine_set:bool = "set"
    m: bool = "--moored"
    speed, x, y: float
    name: seq[string]

It is up to discussion if the auto filled variables would have the rust prefixes on them by default or not... I think it would be better to be consistent and safe, thus always using the prefixes for auto-filled variables, and if the user wants shorter names, he should give them.

@oprypin
Copy link
Member

oprypin commented Apr 21, 2015

I am not planning to do anything about this in the near future. I don't see any perfect solution.

This doesn't have to be a part of the library (and maybe shouldn't seeing as it's a direct port). One can make whatever macro they want based on the returned table.

@ReneSac
Copy link
Author

ReneSac commented Apr 21, 2015

Each port seems to be free to adapt to the syntax of the target language. The only requirement seems to be able to pass the test framework. I think docopt would be much more usable with this. But of course it is your call to work on this or not.

And is your docopt already able to supply the table at compile time? Otherwise your last phrase is false.

And a idea discussed in irc and not yet documented here: use dummy modules to provide convenient and robust namespacing between flags, cmds and args. Better than emulating namespacing via prefixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants