rust-cpython icon indicating copy to clipboard operation
rust-cpython copied to clipboard

Simpler python None

Open icorderi opened this issue 7 years ago • 6 comments

Would you consider adding this impl?

impl ToPyObject for () {
    type ObjectType = PyObject;

    fn to_py_object(&self, py: Python) -> Self::ObjectType {
        Python::None(py)
    }
}

icorderi avatar Apr 30 '17 17:04 icorderi

The problem is that it is unclear what ().to_py_object() should return: In the context of a method return value, it should return None. But in other contexts (e.g. specifying arguments when calling a method), it should return the empty Python tuple ().

In fact, I originally handled () the same way as all other Rust tuples. I ended up introducing struct NoArgs instead precisely to avoid the ambiguous impl ToPyObject for ().

dgrunwald avatar May 01 '17 09:05 dgrunwald

I would argue that rust () always maps to python None. In the context of calling a python method, the signature should be something like this instead:

fn call_method<A>(&self, py: Python, name: &str, args: Option<A>, kwargs: Option<&PyDict>) -> PyResult<PyObject> 
where A: ToPyObject<ObjectType=PyTuple>

The usage then becomes:

  • calling x.call_method(py, "foo", Some(()), None) is equivalent to foo(None) while,
  • calling x.call_method(py, "foo", None, None) will invoke foo() with no args in python.

I would also argue that using Option to represent having args vs not having args is more rustic.

This means one less thing that the user needs to import, making NoArgs not needed as a public type (I'm not sure if you will end up needing it internally).

Another alternative would be take a slice of trait objects:

fn call_method(&self, py: Python, name: &str, args: &[ToPyObject], kwargs: Option<&PyDict>) -> PyResult<PyObject>

Note: I haven't checked if ToPyObject is object safe

The same examples as before now being:

  • x.call_method(py, "foo", &[()], None) will translate to foo(None) while,
  • x.call_method(py, "foo", &[], None) will translate to foo().

icorderi avatar May 05 '17 18:05 icorderi

I think this could solved by introducing ToPyTuple trait. call and call_method could use explicit ToPyTuple instead of generic ToPyObject. () could be resolved to py.None() in case of ToPyObject and to NoArgs in case of ToPyTuple

fafhrd91 avatar May 05 '17 18:05 fafhrd91

I didn't really realize () is also a valid "tuple" in Rust. I guess there is no easy solution here. Maybe Rust should have a separate void type.

quark-zju avatar Nov 24 '17 23:11 quark-zju

I fail to see how having a separate void type would help Rust here. (Also, Rust does have a void type. c_void is part of the C FFI alongside CString where you have to jump through a few hoops to use it to make it clear that it's not to be used for non-FFI uses.)

Heck, in the 15+ years I've been programming Python, I've never consciously encountered an empty Python tuple. (It's possible that some code I used wound up calling tuple() on a list that happened to be empty, but that's about the only meaningful way you can create them, given that you can't create an empty one and then append to it like you can with lists.)

The distinction between None and the empty tuple in Python is basically a minor fix for the type system being that dynamic.

In Rust, the arity of a tuple is part of its type signature, so () is not interchangeable with higher-arity tuples and it doesn't really have any use in Rust itself except serving as a way to indicate void without requring new syntax, since there's exactly one possible instance of the () type and that's (). (Just like None is a singleton.)

ssokolow avatar Nov 25 '17 01:11 ssokolow

I'm fairly certain we don't want () to map to Python None, as empty tuples do exist in python, and I think we want to be clear about the difference between tuple() and None.

We already have a marker struct for the empty tuple in the form of NoArgs so I've been experimenting with a similar marker struct for None:

struct PyNone;
impl ToPyObject for PyNone { ... return py.None() ... }
impl FromPyObject for PyNone { ... }

This lets you write methods like:

    def foo(&self) -> PyResult<PyNone> {
        ...
        Ok(PyNone)
    }

This seems reasonably ergonomic, so I may port this upstream.

markbt avatar Feb 04 '20 11:02 markbt