Idea: Omitting self-explanatory parameter paths via '_'

Idea: Substitution of self-explanatory type parameters via '_'

In many cases, function parameters take self-explanatory structs or enumerations as arguments. In this case, even though the name of the structure is self-explanatory, such as <func_name>Param, if the function is hidden inside a deep dependency, for example, foo::bar::baz::my_fn, the function call will look pretty messy, like foo::bar::baz::my_fn(foo::bar::baz::MyFnParam::new()).

I propose to extend the use of the _ keyword and use it to substitute the type name of a self-explanatory parameter, as shown above. Here's a quick example

pub struct FooParam {
	param1: i32,
	param2: &'static str,
}

impl FooParam {
	pub fn new()
}

fn foo(param: FooParam) {
	// ...
}

// NOW
foo(FooParam::new())
foo(FooParam {
	param1: 1,
	param2: "hello",
})

// TO BE
foo(_::new())
foo(_ {
	param1: 1,
	param2: "hello",
})

This can be implemented simply by replacing _ with its type when the expression provided as a function argument is path and the first segment is _.

These substitutions can be applied equally well to enumerations, and again help to reduce the verbosity of writing self-explanatory forms like the one above.

let mut state = some_very_long_module_name::AppState::FooFoo;

// NOW
state.go_to(some_very_long_module_name::AppState::BarBar);

// TO BE
state.go_to(_::BarBar);

// NOW
some_very_long_module_name::AppState::zoo(
	some_very_long_module_name::AppState::FooFoo + some_very_long_module_name::AppState::BarBar
);

// TO BE
// 'First path segment underscore' of every expression is replaced into the parameter's type
some_very_long_module_name::zoo(_::FooFoo + _::BarBar);

// MODULE DEFINITION ...
mod some_very_long_module_name {
	pub enum AppState {
		FooFoo,
		BarBar,
		BazBaz,
	}

	impl AppState {
		fn go_to(&mut self, next: Self) {
			// ...
			*self = next;
		}

		fn zoo(a: Self) -> Self {
			panic!("...")
		}
	}

	impl std::ops::Add for AppState {
		type Output = Self;

		fn add(self, rhs: Self) -> Self::Output {
			match (self, rhs) {
				_ => panic!("..."),
			}
		}
	}
}

This can be applied to traits in similar fashion.

mod inner {
	pub trait MyTrait: Sized {
		fn pewpew() -> Self {
			self
		}
	}

	pub struct SomeImplementor;
	impl MyTrait for SomeImplementor {}

	fn foo(_: SomeImplementor) {}
	fn nope(_: impl MyTrait) {}
}

// NOW
inner::foo(<inner::SomeImplementor as inner::MyTrait>::pewpew());

// TO BE
inner::foo(<_ as inner::MyTrait>::pewpew());

// NOW
inner::nope(inner::SomeImplementor);

// TO BE: NOPE!
// inner::nope(_); // _ for impl MyTrait: Can't be inferenced!

Or, in builder patterns ...

#[derive(Builder)] // Suppose we've added awesome builder crate dependency
struct AVeryVeryLongStructNameWhichDerivesBuilderForItsConstruction {
	foo: i32,
	bar: i32,
	baz: i32,
}

fn acceptor(s: AVeryVeryLongStructNameWhichDerivesBuilderForItsConstruction) {}

// NOW
acceptor(
	AVeryVeryLongStructNameWhichDerivesBuilderForItsConstruction::builder()
		.foo(1)
		.bar(2)
		.baz(3)
		.construct()
);

// TO BE
acceptor(_::builder().foo(1).bar(2).baz(3).construct());

It'd be nice to have this.

2 Likes
4 Likes

and it is discussed in a fresh topic at this forum - [IDEA] Implied enum types

1 Like

See also Name elision - #7 by josh

Not sure how I feel about _::method() since strictly speaking _ can't be inferred there -- any number of types could have a function method() returning the correct type. But the rest, in particular _ { data } and enum variants, absolutely should be.

Yeah, _::method() has more potential inference issues. That said, we already have mechanisms that could allow inferring many possible types: you can write Default::default() and Rust will infer the right type implementing Default.

I do think _::EnumVariant is more important and useful, though.

3 Likes

Default::default() inference only works because it's -> Self, maybe the same restriction would work for inherent functions too. That wouldn't allow for the builder example.

1 Like

Strictly speaking, no type inference occurs in the above proposal.

The proposal assumes a scenario where you call a function that takes a concrete type as an argument. The '_' is simply replaced by the type of the argument, which is already known, so _::method() would be replaced by ParameterType::method(). I don't think this has anything to do with type inference.

Of course, it's not applicable when taking impl Trait as an argument, but I suspect there will be many uses for it that aren't 'Generic'.

The same can be said for the builder pattern.

struct MyStruct { }

#[derive(Default)]
struct MyStructBuilder { }

fn foo(va: MyStruct) {}

// NOW
foo(MyStructBuilder::default().build())

// TO BE: Of course, can't. 
// foo(_::default().build());
// -> interpretted as foo(MyStruct::default().build()); pointless!

If, as you say, the type and the builder of the type are completely separate types, as above, this substitution would not occur, and type inference from default() would not be possible.

However, if the type of the argument itself is such that it returns the builder, as in the following example, then this could be applied to the builder pattern as well.

impl MyStruct {
  fn builder() -> MyStructBuilder { todo!() }
}

// NOW
foo(MyStruct::builder().build())

// TO BE
foo(_::builder().build());

Of course, we don't know how useful these patterns will be in practice, but I wanted to show that it's possible to do this with simple substitutions :slight_smile:

Oh, that's the exact same thing, sorry. Glad to hear it's being discussed anyway!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.