Skip to content

Add a match_class! macro #771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
OddCoincidence opened this issue Mar 31, 2019 · 6 comments
Closed

Add a match_class! macro #771

OddCoincidence opened this issue Mar 31, 2019 · 6 comments
Labels
RFC Request for comments

Comments

@OddCoincidence
Copy link
Contributor

Summary

Add a match_class! macro for implementing behavior that varies based on the built-in type of an object.

Detailed Explanation

Doing this currently with downcast is a bit awkward. Because it consumes the Rc,
you have two options:

  1. Clone the object and then do a series of ifs.
impl PyFloat {
    fn eq(self: PyRef<Self>, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
        if let Some(i @ PyInt { .. }) = other.clone().downcast() {
            Ok(PyInt::new(&self.value + i.as_bigint()).into_ref(vm))
        } else if let Some(f @ PyFloat { .. }) = other.downcast() {{
            let output = self.value + f.value.to_bigint().unwrap();
            Ok(PyFloat::new(output))
        } else {
            Ok(vm.ctx.not_implemented())
        }
    }
}
  1. Use the returned Err(obj) of downcast to avoid the clone.
match other.downcast() {
    Ok(i @ PyInt { ..}) => {
        Ok(PyInt::new(&self.value + ).into_ref(vm))
    },
    Err(obj) => match obj.downcast() {
        Ok(f @ PyFloat { .. }) => {
            let output = self.value + f.value.to_bigint().unwrap();
            Ok(PyFloat::new(output))
        },
        _ => Ok(vm.ctx.not_implemented()),
    }
}

This avoids a pointless clone at the cost of adding undesirable rightward drift. What we really want is to be able to use a match-like syntax that expands to the efficient version above:

impl PyFloat {
    fn eq(self: PyRef<Self>, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
        match_object!(other,
            i @ PyInt { .. } => {
                Ok(PyInt::new(&self.value + i.as_bigint()).into_ref(vm))
            },
            f @ PyFloat { .. } => {
                let output = self.value + f.value.to_bigint().unwrap();
                Ok(PyFloat::new(output))
            },
            _ => Ok(vm.ctx.not_implemented()),
        )
    }
}

Drawbacks, Rationale, and Alternatives

The main drawback is that it is another macro. It should be very straightforward to implement and to understand though.

Unresolved Questions

Will allowing a full pattern in each match clause encourage making struct fields public, when they really should not be? An alternative would be to just allow the type name in each match arm.

@coolreader18
Copy link
Member

Another question is that this would further cement the unification of class and Rust type and would we want that? I'm not opposed to it personally, but it's something to think about.

Also, instead of using a pat matcher we could just do an $ident @ $path matcher, if we're concerned about public typestruct fields.

@coolreader18 coolreader18 added the RFC Request for comments label Apr 1, 2019
@cthulahoops
Copy link
Collaborator

This worries me a bit. Matching on the internals is something we should do rarely (if ever?) and with great care, so it seems strange to provide a convenient macro to make it easy.

I think the example use case in the PR is wrong. The correct implementation of to_int is something like:

pub fn to_int(vm: &VirtualMachine, obj: &PyObjectRef, base: u32) -> PyResult<BigInt> {
   Ok(vm.call_method(obj.clone(), "__int__")?.value)
}

(Plus whatever is needed to make the base special case work.)

@OddCoincidence
Copy link
Contributor Author

Matching on the internals is something we should do rarely (if ever?) and with great care, so it seems strange to provide a convenient macro to make it easy.

What here are you viewing as "internals"? I would expect all the types and methods used here to (eventually) be part of our public API for extension.

I think the example use case in the PR is wrong. The correct implementation of to_int is something like:

Yeah, a better example would have been to something like complex.__eq__, which needs to work with complex, int, and float.

@cthulahoops
Copy link
Collaborator

What here are you viewing as "internals"? I would expect all the types and methods used here to (eventually) be part of our public API for extension.

My expectation is that only methods on the type should be able to access the value directly everything else has to go through methods. Otherwise I think you're liable to break subclasses by accessing something you shouldn't. From this perspective nothing should ever access the private data of another class.

I haven't thought much about how the shape of our public api, so I'm not sure how extensions are expected to work with values.

Yeah, a better example would have been to something like complex.__eq__, which needs to work with complex, int, and float.

Isn't that's still the same. int and float support imag and real so that interactions with complex work? (Oddly cpython doesn't seem to be duck-typing as I'd expect here, so I'm not sure how this works.)

@OddCoincidence
Copy link
Contributor Author

OddCoincidence commented Apr 2, 2019

My expectation is that only methods on the type should be able to access the value directly everything else has to go through methods.

Completely agree! I assume you're referring then to the full pattern matching in the proposal above (i.e. PyInt { .. })? I noted this in the "unresolved questions" section and in the PR I went with @coolreader18's suggestion to only support a more limited $ident @ $path syntax.

Or by methods did you mean vm.call_method(...)? My general feeling is that this should usually be circumvented for built-in types for performance reasons. I think we'll find that cpython does similar things, and a lot of edge cases that spring to mind won't actually work there.

Isn't that's still the same. int and float support imag and real so that interactions with complex work?

More generally, I think many implementations of __eq__ and other binary methods will have to match on class in one way or another. This just provides a cleaner, more readable, way.

@cthulahoops
Copy link
Collaborator

Ah, yes, I missed that distinction. In that case I guess this is equivalent to an isinstance check. (The examples in the issue call .value the actual implementation calls .as_bigint.)

Of course, this creates another complication: as_bigint now needs to check if it's a subclass, but that's a problem with or without this syntax.

I'm happier with the more limited implementation.

coolreader18 added a commit that referenced this issue Jun 8, 2019
Structure taken from #771
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RFC Request for comments
Projects
None yet
Development

No branches or pull requests

3 participants