5个版本

使用旧的Rust 2015

0.1.1474613452 2016年9月23日
0.1.5 2016年9月23日
0.1.3 2016年9月18日
0.1.2 2016年9月16日
0.1.1 2016年9月16日

#9 in #named-arguments


用于namedarg

MIT许可

97KB
SLoC 2.5K

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语法默认使用unnamed(为了与现有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还是FnFnMutFnOnce特质)中都不适用;类型会丢失参数名称。

    要使用命名参数给函数命名而不调用它,请使用此语法(灵感来源于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);
    

    夜间版生成的错误信息看起来稍微好一些,但无论如何,如果你意外地调用了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的无名参数

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 稳定下来,第一行可以省略。

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

您可以使用 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 进行相同的更改 - 但如果我们忘记了,编译器不会抱怨!在静态语言中,重构函数通常会可靠地在尚未修复的地方产生类型错误,但具有不同默认值永远不会是类型错误。

避免这种问题的一种方法可能是使用一个包装器 "config" 结构体,其构造函数负责处理默认值。(这也通常被提出作为命名参数的一般替代方案。)如果有很多参数,这是一个很好的解决方案 - 毕竟,重复 参数名称 本身就是一个 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 的 bug。尝试通过将 namedarg! 放入宏展开中来解决这个问题并不十分有效——一方面,由于实现基于自定义派生,namedarg! 的内部必须是一组完整的项(例如 fn 函数、结构体声明等),而不是一个表达式。此外,因为 namedarg! 的展开包含 extern crate,它只能指定一次,在 crate 根目录下。你可以通过使用 namedarg_inner! 来解决这个问题,它与 namedarg! 相同,但没有 extern crate:因此,必须在该文件中存在一个 namedarg! 才能使其工作。但完整的项限制仍然存在。我应该添加某种逃逸机制,以更好地解决 rustc bug,允许任何宏都可以在 namedarg! 内定义。

  • 夜间实施是一个荒谬的解决方案。它曾经只是一个常规的夜间编译器插件,但它提供了一个更糟糕的用户体验:无法导入编译器插件,因为 macro_rules! 宏的展开,因此用户代码必须包含逻辑来决定是否使用夜间或 macros-1.1 实现,这给已有的需求增加了许多杂乱。一个理智的人会提交一个 PR 到 rustc 以改进 macro_rules! 和插件之间的交互。相反,我注意到展开 可以 加载 macros-1.1 自定义派生,并编写了一些非常不安全的代码,使一个不稳定的插件伪装成 macros-1.1 插件。

依赖关系