View types. Give borrow checker more information about your code

First of all, I am well aware of this issue, I looked through people's ideas but I did not like any of them. This is my approach to the problem.

Rationale

Borrow a checker is an amazing tool offered by the compiler that catches a scary amount of bugs. It also does not disallow you from anything as with enough context, a borrow checker can let a lot of code pass and let you write the natural way, only protesting when necessary. This of course only really applies when you write everything into a singular function.

Within the boundaries of a function, the compiler is excellent, but when you borrow something from another function, there is not enough information to prove anything. You can only borrow everything or nothing, which tends to force users into two scenarios.

// instead of
self.do_something();
// we have to resort to
Self::do_something(&mut self.this, &self.that, &mut self.and_even_this);
// other option is
{
    // just inline if function is simple enough
    // borrow checker now gets enough context
}

Hacks

Both of the above can be partially solved by hardcoding a macro.

macro_rules! self_do_something {
    ($self:expr) => { 
         Self::do_something(&mut $self.this, &$self.that, &mut $self.and_even_this) 
    };
}

This results in a lot less typing.

// now we can say
self_do_something!(self)

There still stays the problem of declaring the method with numerous parameters. We also have to write the no-brainer macro and we lose the self parameter grouping things together. The next step would be packing all borrowed states into a struct, and constructing them inside the macro.

self_some_view!(self).do_something()

As you can see with some boilerplate and the use of ugly macros, you can use View Types already.

New Kind Of Type

A new type of declaration needs a keyword. I cannot decide what kind of keyword it should be so I will refer to it by #keyword

I propose adding the so-called View types that would be types bound to some parent type, restricting access and mutability. The header of the type can look similar to trait implementation.

$($vis)? $(unsafe)? #keyword $name:ident for $parent:ty { ... }

name signifies the name of view type. All generic parameters have to be inherited from the parent. parent would be the target type, the view is capturing. This can be any type and even another View. unsafe is an annotation that makes accessing view type fields an unsafe operation. Now the body.

<...> {
     // the `as ...` is for optionally specifying the view of given field so nesting is possible
    $($($vis)? $(mut)? $($field_name:ident|$field_number:literal) $(as $view:ident)?),* $(,)?
}

Let's give an example of View type in action.

// immutable struct:
struct A {
   value: i32,
}

#keyword ImmA for A {
    pub value,
}

// main feature of view types is ability to implement methods that all parents inherit.
impl ImmA {
    // here we return owned view type, since `value` is not mut,
    // user can read but not modify the field (without using `unsafe`)
    pub fn new(value: i32) -> Self {
        A { value }
    }
}

fn test() {
    // Here the `new` can be also called on `A`
    let mut a = A::new(20);
    a.value = 10;
    ^^^^^^^^^^^^^ invalid
}

Let's get a little bit more complex.

struct Vec<T> {
    data: VecData<T>, // pointer and cap
    len: usize,
}

impl<T> Vec<T> { ... }

pub unsafe #keyword MutableVecLen for Vec<T> {
    pub mut len,
}

pub #keyword VecLen for Vec<T> {
    // notice that view type can alter visibility
    pub len,
}

#keyword VecIter for Vec<T> {
    mut data,
    len,
}

impl<T> VecIter<T> {
    fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
        // we can now fully access `data` but only immutably access `len`
        ...
    }
}

fn test() {
    let mut vec = vec![1, 2, 3];
    assetr_eq!(vec.len, 3); // no need to call `len()`, or any boilerplate getter for that matter
    unsafe {
        vec.len = 2; // valid within the unsafe block
        vec.len = 3; // lets put it back
    }
    vec.iter_mut().for_each(|e| *e = vec.len); // `iter_mut` borrows the `len` immutably
}

implementation

I do not have a deep understanding of how rust solves things and how would this fit into the codebase or performance implications so bear with me.

The rough idea for visibility would be traversing subtypes until they contain the given field, if no field with correct visibility is found, emit an error. Mutability is a bit different, You cannot define field mutable if it's already defined as immutable in the parent. When type-checking view should be always assignable from the parent and methods callable from any parent up to the View itself. Differentiating unsafe and safe View can be difficult but possible. The most restricting View should be used when inferring. Views are similar to Traits in a way they relate to types. You should be only able to define the view inside the crate target type is defined unless it is possible to restrict this less. Views also have to be distinct, this can be determined by hashing of some sort and lookup.

uncertainty

  • Should Views require import to be used?
  • Should we be able to implement traits on View types?

advantages

  • View types can make the Type system stronger and a lot more expressive.
  • APIs can be improved and made easier and safer to use.
  • Code can be organized into methods with more ease.

alternatives

Some functionality can be simulated by solutions seen in hacks. Mutability and publicity as well but View types seem like idiomatic reduction of boiler-plate.

How is this different to a struct of references?

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