-
-
Notifications
You must be signed in to change notification settings - Fork 129
Description
Update
My views on this topic have evolved, see the comment for an up to date version: #805 (comment)
Following is the initial obsolete comment for the record.
Another Into<Option<_>>
experiment
Obsolete: see update in #805 (comment)
In gstreamer-rs
a fix for nullability inconsistency had an API change induce an update to all the call sites for a function fromq.set_uri(uri)
to q.set_uri(Some(uri))
. This revived the debate whether Into<Option<_>>
should be used in argument position when the argument is optional.
This debate was previously concluded: "Into<Option<_>> considered harmful". The main arguments leading to this conclusion are:
- In some cases, this requires annotations, which can result messy, particularly when dealing with closures.
- Users can be confused and not figure out that the API can also accept
None
.
We wanted to figure out whether the problems encountered at the time should still be considered harmefull and whether Into<Option<_>>
should be reintroduce, at least in some cases, as we ended up doing for set_uri
.
TLDR
The TLDR is the above arguments are still valid:
- Type annotations are still necessary in some cases when we want to pass
None
. We probably don't want to use this with closures or complex function arguments. - When users find code such as
q.set_uri(uri)
, they will probably not guess they can useq.set_uri(None)
too.
Nevertheless, code looks nicer IMO when at least some of these arguments use this feature. I conducted an experiment on gstreamer-rs
changing most candidates in manual code and updated gst-plugins-rs
to use the resulting code base. See the difference in this gst-plugins-rs
commit.
In this issue, I'd like to show the result of a quick experimentation on gtk-rs-core/glib
to illustrate the pros & cons. The changes and some illustrative test cases are available in this gtk-rs-core/glib
commit.
The easy cases
There are easy cases for which there's no issue apart from the users not immediately figuring out that the arguments can be an Option
.
Copy + Clone
types
The Copy + Clone
types handling is straightforward with limited impact on the function signature and body.
See the Channel::new
implementation and the above test cases.
Reference to concrete types
When the argument's inner type is a reference to a concrete type, the implementation is quite straightforward too, with the addition of a lifetime.
See the DateTime::from_iso8601
implementation and the above test cases.
The less easy cases
Reference to a type by one of its trait implementation
This is where it starts to get tricky.
Strings
One very common case for this is when the argument is a string, like in set_uri
. Currently, most functions use an Option<&str>
for these kind of arguments. This allows using:
q.my_function(Some("a `str` literal"));
q.my_function(Some(&a_string)); // reference is mandatory
q.my_function(None); // type for `None` is non-ambiguous
With the changes in ParamSpecBuilderExt::set_nick
, besides the same Some
variants, we get:
q.my_function("a `str` literal");
q.my_function(&a_string); // reference is still mandatory
q.my_function::<str>(None); // type for `None` needs disambiguation
The None
case is unfortunate. It shows up here, but it actually was already an issue for others use cases which lead to the introduction of the NONE
constant for some types.
The signature is a bit ugly, the ?Sized
bound is necessary to accept the str
literal.
I tried to have the signature accept plain String
, but I gave up.
IMO the type annotation annoyance is acceptable compared to the usability improvement. There are many of these in this gst-plugins-rs
commit.
Subclasses
Another common use case involves subclasses. See SignalGroup::set_target
as an example. In this case, with current API, we need to use the NONE
constant:
// type for `None` needs disambiguation
SignalGroup::new(Object::static_type()).set_target(Object::NONE);
Disambiguation is still necessary with Into<Option<_>>
, though we can use this instead:
// type for `None` needs disambiguation
SignalGroup::new(Object::static_type()).set_target::<Object>(None);
Which could render the NONE
constants unnecessary. Note that this could be possible with current API if the type was declared as a generic argument <T: IsA<_>>
instead of an impl IsA<_>
.
IMO, when dealing with subclasses this is useful as it leads to leaner code. It might not be applicable to all functions though.
Functions and closures
The last example combines two types from their traits. One is a Path
, so it is quite similar to the str
case and the other is a function trait. This can be seen in the spawn_async
implementation.
Of course, there's nothing special about the plain and Some
cases. For the None
case, we get to provide type annotation for the None
argument:
// type annotation needed for 1st arg due to `None`
// v
let _res = spawn_async::<path::Path, _>(
None,
&[path::Path::new("test")],
&[],
SpawnFlags::empty(),
|| {},
);
// type annotation needed for both args
// v v
let _res = spawn_async::<path::Path, fn()>(
None,
&[path::Path::new("test")],
&[],
SpawnFlags::empty(),
None,
);
For some reason I can't immediately explain, using a NONE
constant doesn't work:
const SPAWN_ASYNC_FN_NONE: Option<fn()> = None;
// Compilation fails, but shouldn't IMO:
// cannot infer type v
let _res = spawn_async::<path::Path, _>(
None,
&[path::Path::new("test")],
&[],
SpawnFlags::empty(),
SPAWN_ASYNC_FN_NONE,
);
This is getting tricky. The function signature is mimimalist here, but it would become unacceptable to impose a full signature to users who only want to pass None
is the first place.
Conclusion
IMO we should use Into<Option<_>>
on a case by case basis:
- It was useful for the string arguments in all the cases I encoutered so far.
- It is also useful for the subclass traits args IMO. In the MR for the experiment on
gstreamer-rs
, we started discussing cases where we may not want to do it. - We should probably avoid this for the functions and closures args, except for really simple cases.
Apart from the manual code, applying this to automatically generated code could be useful, so a change to gir
would be necessary.