Anyway to shorten `.into()` to `.i`?

I just created an example shortening it to .i(), when I re-read your question asking to shorten it to i (without parenthesis). Anyway, maybe other readers are interested in how it works with .i(), so I'm sharing my already made up example code:

mod short_into {
    pub trait IntoExt<T>: Sized
    where
        Self: Into<T>,
    {
        fn i(self) -> T {
            self.into()
        }
    }
    impl<T, U: Into<T>> IntoExt<T> for U {}
}

use short_into::IntoExt;

fn main() {
    const N: i16 = 5;
    let n: i32 = N.i();
    println!("Hello {}", n);
}

(Playground)

Output:

Hello 5

I assume it's technically possible to shorten it to i with a function-like procedural macro. Something like:

into_as_i!({
    const N: i16 = 5;
    let n i32 = N.i;
    n
})

But not sure how such a macro could be defined, but I think it's possible, isn't it?

P.S.: I wouldn't want to use that syntax myself, just asking out of curiosity.

Yeah, upon rethinking this, .i , without the (), does seem a bit unreasonable.

So the job to be done in that case would be to replace things in the AST that look like field accesses with the name i with calls to into.

The syn crate has an example of how to replace parts of the AST. For this macro, instead of matching on Expr::Lit you would match on Expr::Field, and only do the replacement if the contained ExprField's Member field contains an Ident that returns "i" from its to_string method.

So it's fairly straightforward to define this macro. Whether it would be a good idea to use the macro is a separate question, of course.

1 Like

I believe you're thinking about closures doing partial (field) captures.

1 Like

Ah yes.

Macro could do better.

I tried to implement CustomInitialize and CustomAssign trait in both my toy crate and this forum, which allow using

fn main() {cai!{// Custom Assign and Initialize, or "菜" in Chinese which suggest my programming skill is not good enough.
    let a:i32:=1u8;
    let mut b=1i32;
    b~a;
    b:=a;
}}

by define the corresponding trait correctly.
no need to write Neg trait which actually do not support convert the base type to different types.

Have you seen the trait ? It defintly looks like you can specify an arbitry Output

Your macro is cool but you have to write everywhere you need it which doesnt feel good at all, compare that with magic ! ! everywhere

Actually neg is the unary - so that would look like - - everywhere instead,

I guess std::ops::not looks better ! ! !

the problem is that, you couldn't both impl Not<Output=u32> for T and impl Neg<Output=i32> for T for the same T:

error[E0107]: this trait takes 0 generic arguments but 1 generic argument was supplied
 --> src/main.rs:2:16
  |
2 | impl std::ops::Not<i32> for T{
  |                ^^^----- help: remove these generics
  |                |
  |                expected 0 generic arguments

with the generics removed:

  |
2 | impl std::ops::Not for T{
  | ------------------------ first implementation here
...
8 | impl std::ops::Not for T{
  | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `T`

you could make -x or !x have its own type diifferent from x, but that type is unique, you could not re-define them

I once tried that in a unsuccessful github PR for a toy crate. That is useful when convert a BuildConfig to an exact Build since the convert should be unique.
But for .into(), the result should not be unique.

I believe that, if I learn proc_marco in future, I may have the ability to implement a new marco that allow the following code works:

cai!{
fn main(){
    let a:i32:=1u8;
    let mut b=1i32;
    b~a;
    b:=a;
}
}

Could you share an example in playground? Errors further in the post does not correspond with this claim.

I am mainly talk about the idea changing x.into() into !x: that is not possible since .into() could have more return type than std::ops::Not

struct T(i32);
impl std::ops::Not for T{
    type Output=i32;
    fn not(self)->Self::Output{
        self.0
    }
}
impl std::ops::Not for T{
    type Output=u32;
    fn not(self)->Self::Output{
        self.0 as u32
    }
}
fn main(){
    println!("{}",!T(1));
}
// this trait takes 0 generic arguments but 1 generic argument was supplied
struct T(i32);
impl std::ops::Not<i32> for T{
    type Output=i32;
    fn not(self)->Self::Output{
        self.0
    }
}
impl std::ops::Not<u32> for T{
    type Output=u32;
    fn not(self)->Self::Output{
        self.0 as u32
    }
}
fn main(){
    println!("{}",!T(1));
}
1 Like

Late to the party and going off on a tangent ...

This is the reason for the Java/C++ dogma of Thou shalt make all data members private: following this rule prevents clients from being able to write let area = shape.area in the first place.

In these languages, you can hide a field access behind a function-like interface, while you cannot hide function calls behind field-access-like syntax, so you make everything look like a function call. The price is having to write countless trivial getters and setters for most of your data. The reward is that you can change your mind about implementation details, without breaking client code.

Python gets around the problem with properties which make field-access syntax invoke functions: This allows you to hide function calls behind field-access syntax, as well as hiding field access behind function calls, so it doesn't matter whether you start off representing area as a field or as a function: you can always keep up the pretence that it's still there, even after you've changed your implementation. Thus public data are perfectly acceptable in the interface of your classes in Python, which really freaks out those who grew up in Java or C++. I think that C# has something similar.

Bertrand Meyer considered this whole business to be crucially important to good software design, so he called it the Uniform Access Principle:

All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation.

and made it one of the cornerstones of Eiffel.

I haven't written enough Rust to come up with an opinion about how to address this in Rust, but whenever I write a type with a pub field, I do feel slightly uncomfortable, for exactly the reasons you mention.

1 Like

Ah yes, Eiffel. Thought I'd seen Uniform Access somewhere. Had his book on Eiffel back in the day. Did anyone ever use Eiffel? I have never seen it in the wild.

I guess I should have spotted it in Python and/or C# but I have been trying to avoid those as much as possible.

Of course the next thing is that array access should look the same as a function call with a single usize parameter.

Common (and other) Lisps do that: aref. But then Lisps do everything with the same single syntax, which is their fundamental strength.

I recall array access and function calls having the same syntax elsewhere ... MATLAB? Surely somewhere else too.

Anders Hejlsberg added properties to Delphi years before he joined Microsoft and developed C#.

Fortran is a tiny bit older, I would think. Although I'm not sure if it had functions back in 1957, or they were added later. This development happened many years before I was born, you see.

It would be interesting to know if Lisp did that before Fortran or after but it would have only purely theoretical interest: this happened before languages theory was developed, compilers were very ad-hoc and nobody was thinking twice about the fact that two very different constructs have the same syntax.

Note that Rust has several features that (almost necessarily) depend on field access instead of methods:

  • Pattern matching, destructuring
  • Partial moves out of owned structures
  • Simultaneous borrows of different parts of a structure

I don't mean to say that one should always prefer these over the Uniform Access Principle, but that it's important to consider how the type is going to be used and if these are desirable. Sometimes a type is so specifically defined that switching it to have computed results would not even make sense.

1 Like

I can see the value in it, but there is also a cost. By obscuring the cost of an operation it is less obvious where something that might be expensive is being done. In general, I prefer APIs that makes the cheap simple and the expensive clearly so.

Using getters and setters has been my policy for exactly the reasons @jacg gives. Unfortunately, I've run into borrow check errors with Rust. I'm hoping Polonius will fix them.

If we're talking about the function-call vs. field-access distinction, I fully expect compilers (of languages where performance is frequently a major concern) to inline (or even completely eliminate) some function calls, so seeing a syntactic function call tells me nothing whatsoever about the cost of the operation. This would leave us (in a language without a property-like mechanism) with the distinction between the cost of

  • a field access,
  • absolutely anything at all.

So I don't find this distinction useful at all.

Getting back on-topic, I do find that the difference between things like .i and .into() or .x and .get_x() can be significant for the signal-to-noise ratio of the code, though not necessarily in a positive way. In the specific case of .i vs .into() in Rust, I suspect that .into() is the better choice.

Whereas being able to change your mind about compute-vs-store (etc.) implementation details without breaking client code, is a huge boon. Even though I would never claim that 'all those millions of Java and C++ programmers can't be wrong' I do believe that the all-data-private rule is the usually the right choice in those languages.

The borrow checker is not allowed to use information about function bodies, only signatures, when checking a call to the function — that's a rule about what is public versus implementation detail. Polonius will not change that.

2 Likes