7个版本

使用旧的Rust 2015

0.1.1474613452 2016年9月23日
0.1.5 2016年9月23日

#1646 in Rust模式


2个crate中使用了(通过namedarg_rustc_macro

MIT许可证

3KB

crates.io badge

Rust的命名参数

一个实验性的(可能已损坏)过程宏。而不是像这样的代码

parser.parse_item(vec![], true, false)

这个宏让你可以写这样的代码!

parser.parse_item(attrs: vec![], macros_allowed: true, attributes_allowed: false)

部分作为未来Rust功能的潜在证明概念编写,但同时也为了实用和可用。某种程度上。理论上。

语法

强烈受Swift启发。

与Swift一样,参数名称是函数签名的一部分,不能重新排序;此外,一个参数可以有独立的'外部'和'内部'名称,其中前者用于调用函数,后者是结果的变量名称。

实际上,声明语法与Swift非常相似,但有两个形式互换位置

变体 Rust Swift 调用方式 变量名称
无名称 fn foo(bar:String) funcfoo(_bar: String) foo(string) bar
基本命名 fn foo(_ bar:String) funcfoo(bar: String) foo(bar:string) bar
不同的名称 fn foo(barbaz:String) funcfoo(bar baz: String) foo(bar:string) baz

Swift默认使用命名参数,并使用_表示未命名参数,而扩展的Rust语法默认为未命名(与现有Rust代码兼容),并使用_表示命名参数,具有等效的外部和内部名称。对于不同的外部和内部名称,请用空格分隔。

命名参数通过在定义和调用位置都将参数名称附加到函数名称来实现。

  • 你可以使用相同名称但不同参数标签重载函数。这不像在C++/Java等中看到的纯基于类型的重载那样有缺陷,所以没有不使用它的理由。例如,考虑构造函数。对于零个和单个参数的构造函数,Rust的现有约定::new()::with_foo(foo)工作得相当好

    HashMap::new()
    HashMap::with_capacity(100)
    

    但是,对于两个或更多参数,它开始变得丑陋

    HashMap::with_capacity_and_hasher(100, Default::default())
    

    在具有命名参数的API中,进展可以更加自然

    HashMap::new()
    HashMap::new(capacity: 100)
    HashMap::new(capacity: 100, hasher: Default::default())
    

    (注意:也可以区分不同数量的未命名参数,但只有在第一个命名参数之后。)

  • 命名参数语法既适用于裸函数也适用于特例方法(在特例定义和实现中)。但在lambda表达式或函数类型(无论是裸函数fn(...)还是Fn/FnMut/FnOnce(...)特例)中都不适用;类型会丢失参数名称。

    要使用命名参数来命名一个函数而不调用它,请使用以下语法(灵感来源于Objective-C)

    调用方式 命名为
    foo() / foo(123) foo
    foo(bar: 123) foo{bar:}
    foo(123,baz: 456, 789) foo{:baz::}

    然后可以不使用标签或强制转换为适当的fn类型来调用它。

  • ...错误消息可能会有些令人困惑。我这里的“附加到函数名”是字面意思。就像任何其他Rust过程宏一样,这个宏执行纯语法转换,然后将一些输出代码交给编译器,之后几乎没有控制编译的能力。以下输入

    fn foo(_ bar: u32) {}
    foo(bar: 5);
    

    被转换为

    // In "nightly" mode:
    fn foo{bar:}(bar: u32) {}
    // ^^^^^^^^^ this whole thing is stuffed into an Ident token so it's treated as
    //           a name, even though it's not a valid ident in vanilla Rust)
    // ditto for the call:
    foo{bar:}(bar: 5);
    
    // In "macros 1.1" mode, we have to use valid code:
    fn foo_namedarg_bar(bar: u32) {}
    foo_namedarg_bar(5);
    

    nightly版本会产生更易于阅读的错误,但无论如何,如果你不小心调用foo(5)(没有参数名称),错误消息将只是说 无法解析名称 `foo`。它不知道你只是弄错了标签。(所以不要忘记!)

    注意:如果能通过lint改进这一点那就好了,但早期lint太早了,而编译失败时不会运行晚期lint。

模式

在Rust中,与Swift不同,参数实际上没有'名称';它们有'模式',就像letmatch一样。所以这是有效的标准Rust

fn foo(Config { a, b }: Config) {}
fn foo(TupleStruct(a, b): TupleStruct) {}
fn foo((a, b): (i32, i32)) {}

使用这个宏,模式可以与命名参数一起使用...有时

fn foo(config Config { a, b }: Config) {} // OK
fn foo(ts TupleStruct(a, b): TupleStruct) {} // OK
fn foo(pair (a, b): (i32, i32)) {} // No good...

不幸的是,最后一个创建了一个语法歧义:它打算是一个名为pair的参数,该参数接受一个元组,但它已经是Rust中接受类似元组的结构体pair的无名参数的合法Rust语法

struct pair(i32, i32); // Lowercasing a struct name is bad style, but possible!
fn foo(pair(a, b): pair) {} // Only the space distinguishes this from the other example.

(嗯,还有类型,但使用类型区分不会起作用,因为存在类型别名。)

目前,唯一的解决方法是将解构移动到函数体中

fn foo(pair pair: (i32, i32)) { let (a, b) = pair; ... }

这已经足够好,但不够优雅。这个插件未来的版本可能会改变语法以消除歧义。(Rust的任何官方命名参数特性可能也应该做同样的事情...)

与类型注解的交互

理论上,命名参数调用语法可能与类型注解(目前不稳定)冲突。表达式foo(a: b)包含一个名为a的命名参数,其值为b,还是一个未命名的参数,其值是将变量a转换为类型b

在这个插件中,命名参数语法仅在函数参数的开始是一个标识符后跟冒号时才会触发。因此,当表达式不是普通变量时,类型注解不受影响: foo(5: usize)foo(x.collect(): Vec<bool> 运行正常。如果表达式是变量,则可以添加括号: foo((a: usize))

但是,如果你想想,你为什么要一开始就给变量类型注解呢?在几乎所有情况下,给变量定义一个类型注解而不是使用类型注解会更好。例外情况可能包括在复杂模式或匹配语句中定义的变量,这些变量不能给出类型,但即使在这种情况下,通常也有更好的编写方式。

用法

将其添加到您的 Cargo.toml 中

[dependencies]
namedarg = "0.1"
namedarg_rustc_macro = "0.1"

并使用宏调用包裹您的源文件

#![feature(rustc_macro, custom_derive)]
#[macro_use] extern crate namedarg;
namedarg! {

// your code here

}

是的,这有点丑。一旦宏 1.1 稳定下来,第一行可以省略。

"Macros 1.1" 模式会在检测到非夜间 rustc 时自动启用,尽管在撰写本文时这毫无用处,因为它尚未真正稳定。它有一个 轻微 的缺点是会破坏 namedarg! 内部所有内容的行号,这是宏 1.1 设计中的固有缺陷。比如,所有错误都会指向包含 namedarg! 的行。这并不适用于开发,但(在稳定后)应该可以在夜间版本上开发一个crate,并以这种方式在稳定版本上编译。

您可以使用 Cargo 功能强制启用宏 1.1 模式

[dependencies.namedarg]
version = "0.1"
features = ["force_macros11_mode"]
[dependencies.namedarg_rustc_macro]
version = "0.1"
features = ["force_macros11_mode"]

默认参数

还有一个用于 非常简单 且更具实验性的默认参数语法,即可以省略默认值的参数。我,comex,喜欢Swift的命名参数语法,但我不满意我所知道的语言处理默认参数的方式。问题是当你编写将参数转发给其他函数(通常是称为'便利函数'的小型包装器)的函数时。例如,假设我们有这个Swift代码

struct Player {
    func doSomething(useFlubber: Bool = false) {}
}

func doSomethingToPlayer(username: String, useFlubber: Bool = false) {
    // Convenience function to lookup a Player by name and call doSomething
    Player(username: username)!.doSomething(useFlubber: useFlubber)
}

它运行正常,但我们不得不两次写入默认值,违反了DRY原则。如果我们后来决定更改 doSomething 的默认值,我们可能想要对 doSomethingToPlayer 也进行相同的更改 - 但如果我们忘记了,编译器不会抱怨!在静态语言中重构函数通常相当可靠地会在我们尚未修复的地方产生类型错误,但具有不同默认值永远不会是类型错误。

避免这种问题的一种方法可能是使用一个包装 "配置" 结构体,其构造函数负责处理默认值。(这通常也被提出作为命名参数的一般替代方案。)如果有许多参数,这是一个好解决方案 - 毕竟,重复 名称 已经是DRY原则的违反 - 但在只有一个或两个参数需要转发的情况下,这感觉相当冗长。

相反,我在这个宏中实现的默认参数版本基于Option。它很简单:如果您在参数名前放置 #[default],则默认通过 None 传递。它看起来像这样

impl Player {
    fn do_something(&self, #[default] _ use_flubber: Option<bool>) {
        let use_flubber = use_flubber.unwrap_or(false);
    }
}
fn do_something_to_player(username: &str, #[default] _ use_flubber: Option<bool>) {
    Player::new(username: username).unwrap().do_something(use_flubber: use_flubber)
}
do_something_to_player("someone"); // implicitly passes None
do_something_to_player("someone", Some(true)); // kinda verbose but...

注意:未命名的参数也可以设置默认值。

当前实现的默认值有两个显著的限制。第一个限制在上面的代码片段中很明显:如果你想传递值而不是使用默认值,你必须明确地写出Some,这样会更啰嗦。

第二个限制是,你不能跳过默认参数并指定后续的命名参数

fn foo(#[default] _ a: Option<bool>, #[default] _ b: Option<bool>) -> u32 { /* ... */ }
foo(b: Some(true)) // doesn't work!
foo(a: None, b: Some(true)) // you have to do this.

在解释这些限制的来源之前,我想先展示一下转换是如何工作的。上面的定义会被展开成一系列重载函数

fn foo() -> u32 { foo(a: None, b: None) }
fn foo(_ a: Option<bool>) -> u32 { foo(a: a, b: None) }
fn foo(_ a: Option<bool>, _ b: Option<bool>) -> u32 { /* ... */ }

会生成一系列占位函数,每个参数列表的前缀对应一个,它们将转发到真正的函数。在函数重命名方案下,没有更好的方法(而且就我所知,我认为没有特别好的函数重命名替代方案,尽管有一些涉及到更复杂类型的理论可能性)。

因此,第二个限制是:如果支持跳过默认参数,就必须为参数列表的每个子集生成一个占位函数,而不是每个前缀,这样添加一个参数就会使占位函数的数量加倍;这可能会很快变得失控。虽然我不太建议一开始就使用具有大量默认参数的函数(正如我之前所说,最好使用结构体),但我并不喜欢在这样函数上直接失败的实现。

第一个限制,需要写出Some,将会更容易克服。首先,我们可以想象生成的重载函数不使用Option,比如

fn foo() -> u32 { foo(a: None, b: None) }
fn foo(_ a: bool) -> u32 { foo(a: Some(a), b: None) }
fn foo(_ a: bool, _ b: bool) -> u32 { foo_IMPL(a: Some(a), b: Some(b)) }
fn foo_IMPL(_ a: bool, _ b: bool) -> u32 { /* ... */ }

但在转发时,我们该如何传递Option呢?

fn x_foo(#[default] _ a: Option<bool>) {
  foo(a: a) // type error: expected bool, found Option<bool>
}

对于这个问题,当(明确地)跳过一个参数时,我们该如何传递None呢?

必须有一些特殊语法来表示“传递这个Option参数而不做任何改变”,尽管即使这样也会很复杂(可能需要生成更多的占位函数或改变命名参数的转换)。

另一种可能性是依赖类型系统来区分Option和非Option参数

trait OptionOrJust<T> { ... }
impl<T> OptionOrJust<T> for T { ... }
impl<T> OptionOrJust<T> for Option<T> { ... }
fn foo<A: OptionOrJust<bool>>(_ a: A)

// both work!
foo(a: true);
foo(a: Some(true));

如果参数本身是Option类型的,这甚至可以正确工作...通常。但有时它会变得很混乱

// original:
fn foo<T>(#[default] _ x: T) { ... }
// transformed by the plugin:
fn foo<T, X: OptionOrJust<T>>(_ x: X) { ... }
// but wait...
foo(x: Some(123)) // what is T??
foo::<u32>(x: Some(123)) // error: too few type parameters provided
foo::<u32, _>(x: Some(123)) // OK, but what is this phantom type parameter?

...即使在更好的情况下,它可能会使类型推断变得更差。

其他限制

  • macro_rules!namedarg!中无法正常工作,这是因为一个rustc错误。通过将namedarg!放在宏展开中等方式来尝试解决这个问题效果不佳——一方面,由于实现基于自定义派生,namedarg!的内部必须是一组完整的项(比如fn函数、结构体声明等),而不是一个表达式。此外,由于namedarg!的展开包含extern crate,它只能指定一次,即在crate根处。你可以通过使用namedarg_inner!来解决这个问题,它和namedarg!一样,但没有extern crate:因此必须在同一文件中存在一个namedarg!才能使其工作。但完整的项限制仍然存在。我应该添加一些逃逸机制,以更好地解决rustc错误,允许任何宏都可以在namedarg!内部定义。

  • 夜间构建实现是一个荒谬的破解。它曾经只是一个普通的夜间构建编译插件,但这样却带来了更差的用户体验:无法让 macro_rules! 宏导入编译插件,因此用户代码必须包含决定是否使用夜间或 macros-1.1 实现的逻辑,这在上面的基础上又增加了很多混乱。一个理智的人会向 rustc 提交一个 PR 以改善 macro_rules! 和插件之间的交互。我注意到扩展 可以 加载 macros-1.1 自定义派生,然后编写了一些非常不安全的代码,使一个不稳定插件伪装成 macros-1.1 插件。

无运行时依赖