Syntactic Musings on View Types
— 2025-04-04

  1. edit: 2026-03-23

Here's a silly little insight I had the other day: if you squint, both View Types and Pattern Types seem like lightweight forms of Refinement Types 1. Both will enable constraining types, but in slightly different and complementary ways. Let's take a look at this using an example of an RGB struct containing fields for individual Red, Green, and Blue channels stored as usize 2:

1

I like the term "lightweight refinement types" for the category of extensions that include pattern types and view types. To me it's reminiscent of lightweight formal methods: less thorough than the full thing, but with incredible returns for the relative effort expended.

2

Don't question why we're storing these values as usize too much. It's a little silly. For the purpose of this example just pretend there is some reason why we have to do it like this.

#[derive(Debug, Default)]
struct Rgb {
    r: usize,
    g: usize,
    b: usize,
}

Pattern types will give us the ability to directly use things like ranges or enum members in type signatures. In the following example the type usize is refined to statically only be allowed to contain values between 0..255. This is similar to the NonZero* types in the stdlib, but as part of the language and usable with arbitrary patterns:

impl Rgb {
    fn set_red(&mut self, num: usize is ..255) { .. }
    //                                   ^^^^^^^^ range pattern
}

View types are about segmenting the fields contained in self so that multiple (mutable) methods can operate on the same type without resulting in borrow checking issues. In the following example we provide mutable access to individual fields using separate methods. None of these methods take overlapping borrows of the fields in Self. This means we're free to call all of these methods, observe their return types, and we won't get any borrow checker errors. Here's an example using the syntax from Niko's latest post:

impl Rgb {
    fn red_mut(self: &mut { r } Self) -> .. { .. }
    //                    ^^^^^ view
    fn green_mut(self: &mut { g } Self) -> .. { .. }
    //                      ^^^^^ view
    fn blue_mut(self: &mut { b } Self) -> .. { .. }
    //                     ^^^^^ view
}

Here's a fun question: what happens if we combine combine Pattern Types and View Types? Both serve different purposes, and I know I've got cases where I'd like to combine them. So what would that look like? In the abstract, it seems like we would end up with something like the following:

impl Rgb {
    fn foo(self: &mut { r, g } Self is Self { r: ..255, g: ..255, .. }) {}
    //                ^^^^^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                  view                      pattern
}

To me that seems like a lot to read. But also rather... double? Both View Types and Pattern Types refine Self here. I would have expected us to be able to combine both. Now what if View Types and Pattern Types instead shared the same syntactic position. There's a reason View Types have to use is, so let's use that. With that we could rewrite our earlier example like this instead:

impl Rgb {
    fn foo(&mut self is Self { r: ..255, g: ..255, .. }) {}
    //               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                          view + pattern
}

This seems a lot more readable to me. Combining both features seems much less like a hassle here, and maybe even… nice? This updated notation would also affect our earlier View Types example. Using the updated notation would now be written the same way, but without using any patterns:

impl Rgb {
    fn red_mut(&mut self is Self { r, .. }) -> .. { .. }
    //                           ^^^^^^^^^ view
    fn green_mut(&mut self is Self { g, .. }) -> .. { .. }
    //                             ^^^^^^^^^ view
    fn blue_mut(&mut self is Self { b, .. }) -> .. { .. }
    //                            ^^^^^^^^^ view
}

I feel like this would rather neatly unify where we talk about refinements in function signatures. Notationally this reframes View Types as Pattern Types with ignored fields. Though we don't necessarily need to adopt this notation: what I care about most is that we think about how both features are intended to come together in the language to create a cohesive experience. Thanks for reading!

Edit: 2026-03-23

Niko just added two entries to his "view types" series, describing what he calls “maximally minimal view types” 3. I wrote a bit about it on Zulip, but I figured I should update this post with that too. Let’s start by adapting our last example to the proposed view types notation:

3

The title reminds me of PC Walton’s classic Maximally minimal typeclasses.

impl Rgb {
    fn red_mut(&mut self.{ r }) -> .. { .. }
    //                   ^^^^^ view
    fn green_mut(&mut self.{ g }) -> .. { .. }
    //                     ^^^^^ view
    fn blue_mut(&mut self.{ b }) -> .. { .. }
    //                    ^^^^^ view
}

Which if then combined with pattern types, presumably would look like this:

impl Rgb {
    fn foo(&mut self.{ r: ..255, g: ..255 }) {}
    //               ^^^^^^^^^^^^^^^^^^^^^^
    //                      view + pattern
}

Personally I don’t love the omission of the is keyword here. I think that’s a strong indicator that we’re refining types. But I also don’t want people to write super verbose statements like:

impl Rgb {
    fn foo(&mut self.{ r: usize is ..255, g: usize is ..255 }) {}
    //               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                            view + pattern
}

So maybe we’re ok not having the is keyword and that’s ok. Or perhaps a better fit here would be to lean in further to view types being a form of refinement, and replacing the dot with the is keyword directly:

impl Rgb {
    fn foo(&mut self is { r: ..255, g: ..255 }) {}
    //               ^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     view + pattern
}

Personally I like how consistent this feels. is feels pretty punchy but is also clearer than the dot notation (user: “What is the is keyword in Rust?”). And notably we’re also still omitting the rest-pattern that we assumed we needed earlier in our blog post. I think it’s okay to part with that for brevity. Finally we can update our earlier example again:

impl Rgb {
    fn red_mut(&mut self is { r }) -> .. { .. }
    //                   ^^^^^^^^ view
    fn green_mut(&mut self is { g }) -> .. { .. }
    //                     ^^^^^^^^ view
    fn blue_mut(&mut self is { b }) -> .. { .. }
    //                    ^^^^^^^^ view
}

Opinions might differ, but I quite like this personally. I think it is much more brief than what we originally at in this post, while still retaining a large amount of expressivity and consistency:

fn red_mut(&mut self is Self { r, .. }) { .. }  // 1. Original in this post
fn red_mut(&mut self.{ r }) { .. }              // 2. Simplified using dot-notation
fn red_mut(&mut self is { r }) { .. }           // 3. Simplified using is-notation