Rust traits and their (lack of) privacy

Rust traits and their (lack of) privacy

rust, trait, privacy, haskell

2018-11-27 04:00:00 UTC, by Dimitri Sabadie


On traits privacy interfaces

I’ve been facing an issue several times in my Rust lifetime experience and never ever have come up with a pragmatic solution to this problem yet. This problem comes up as follows:

As you can see, we have a situation here. Rust doesn’t allow to expose a trait without showing its internals. Imagine the following code:

pub fn a_cool_function<T>(id: usize, foo: T) where T: Foo {
  // …
}

pub trait Foo: Sized {
  type Cons: Sized;

  fn compute(self, a: Self::Cons) -> Self;
}

impl Foo for String {
  type Cons = char;

  fn compute(mut self, a: Self::Cons) -> Self {
    self.insert(0, a);
    self
  }
}

impl<T> Foo for VecDeque<T> {
  type Cons = T;

  fn compute(mut self, a: Self::Cons) -> Self {
    self.push_front(a);
    self
  }
}

We want to export a_cool_function. That function accepts as second argument a type that must implement the Foo trait. In order for the function not to leak private symbols, Foo then must be public. However, we don’t want to expose its internals (the Cons associated type nor the compute method). Why we would want to hide those? I have several points:

Currently, you can perfectly make a type public without exposing all its methods as public. Why not having the same power with traits?

There is a – non-ideal – solution to this problem: #[doc(hidden)] on each items to hide from the trait. Items tagged with that annotation won’t show up in the documentation, but they will be definitely usable if a crafty developer reads the source. Not a very good solution to me, thus.

Pre-RFC: Enhanced trait privacy

It’s been a while since I’m looking for a good RFC to introduce this in Rust. This is some needed jargon, so let’s explain a few terms first:

Currently, when you implement Display, you implement the trait. The fmt function you implement is part of its trait definition. When you write a function like fn show<T>(x: T) where T: Display, here, Display is not a trait: it’s a bound.

Bounds are interesting, because you cannot directly manipulate them. They only appear when you constrain a function or type. You can combine them, though, with the + operator:

fn borrow<'a, T>(x: &'a T) -> MyBorrow<'a, T> where T: Display + 'a

This example shows you that lifetimes can be used as bounds as well.

The idea of this RFC is to make a clear distinction between Display as trait and Display as bound so that it’s possible to use a trait only in bounds position and not implementation. One major avantage of doing so is to bring completely new semantics to Rust: exposing a trait as public so that people can pick types that implement the trait without exposing what the trait is about. This brings a new rule to the game: it’s possible to create ad hoc polymorphism that doesn’t leak its definition.

The idea is that we love types and we love our type systems. You might come across a situation in which you need to restrict the set of types that a function can use but in the same time, the implementation of the trait used to restrict the types is either unsafe, or complex, or depends on invariants of your crate. In my spectra crate, I have some traits that are currently public that leak rendering dependencies, which is something I really dislike.

As a prior art section, here’s the wanted feature in Haskell:

{-# LANGUAGE FlexibleInstances #-} -- don’t mind this
{-# LANGUAGE TypeFamilies #-} -- this either

module Lol (
    Foo -- here, we state that we only export the typeclass, not its definition
  ) where

import Data.Text (Text, cons)

class Foo a where
  type Cons a :: *

  compute :: Cons a -> a -> a

instance Foo [a] where
  type Cons [a] = a

  compute = (:)

instance Foo Text where
  type Cons Text = Char

  compute = cons

Trying to use either the Cons associated type or compute function in a module importing Lol will result in a compiler error, because those symbols won’t be accessible.

What it would look like in Rust?

Currently, there is a weird privacy rule around traits. People not coming from Haskell might feel okay about that, but I learned Rust years ago while being already fluent with Haskell and got stunned at this (and I still have microseconds of “Wait, do I not need a pub here? Oh yeah, nah nah nah.”) When you declare a trait as pub trait …, everything in its definition is automatically pub as well.

This is so weird because everything else in Rust doesn’t work this way. For instance:

struct Bar; // here, Bar is not pub, so it’s private and scoped to the current module it’s defined in

pub(crate) struct Zoo; // not public either but can be used in other modules of the current crate

pub struct Point { // public
  pub x: f32, // public
  pub y: f32, // public
}

pub struct File { // public
  inode: usize // private
}

enum Either<L, R> { // private
  Left(L), // private
  Right(R), // private
}

pub enum Choice<L, R> { // public
  Left(L), // public (*)
  Right(R), // public (*)
}

// (*): enums require their variants to be public if they’re public for obvious pattern-matching
// exhaustiveness reasons

pub struct Foo; // public

impl Foo {
  fn quux(&self); // not public, only callable in the current module

  pub(crate) fn crab_core_is_funny(self) -> Self; // not public but callable from within this crate

  pub fn taylor_swift() -> Self; // public, callable from the crate and dependent crates
}

But:

trait PrivTrait { // private trait
  fn method_a(); // private
  fn method_b(); // ditto
  pub fn method_c(); // compilation error and wouldn’t make sense anyway
}

pub trait PubTrait { // public trait
  fn method_a(); // public, even without the pub privacy modifier!!!
  pub(crate) fn method_b(); // won’t compile
  pub fn method_c(); // won’t compile
}

To me, it would make much more sense for Rust to authorize this:

pub trait PubTrait { // public trait
  fn method_a(); // private, only usable from this module
  pub(crate) fn method_b(); // callable only from modules from this crate
  pub fn method_c(); // public
}

However, I know, I know. Turning this feature on would break pretty much everyone’s code. That’s why I think – if people are interested by this feature – we should instead go for something like this:

trait PrivTrait { // private trait
  fn method_a(); // private
  pub(crate) fn method_b(); // compilation error: the trait is private
  pub fn method_c(); // compilation error: the trait is private
}

pub trait PubTrait { // public trait
  fn method_a(); // public (backward compatibility)
  pub(crate) fn method_b(); // callable only from modules from this crate
  pub fn method_c(); // public, akin not to use the pub modifier
  priv fn method_d(); // private; only callable from this module
}

I’d like to point the reader to this issue. In its foreword, @alexcrichton rightfully explains that removing the priv keyword was great because it has yielded a rule ever since – quoting him:

“I think that this would really simplify public/private because there’s one and only one rule: private by default, public if you flag it.”

I feel uncomfortable with the current trait situation because that rule has been broken. This other issue pinpoints the problem from another look: traits shouldn’t set their items visibility based on their own visibilities. This yields weird and unexpected code rules and precludes interesting design semantics – the one I just described above.

I hope you liked that article and thoughts of mine. I might write a proper RFC if you peeps are hyped about the feature – I have personally been wanting this for a while but never found the time to write about it. Because I care – a lot – about that feature, even more than my previous RFC on features discoverability, please feel free to provide constructive criticism, especially regarding breaking-changes issues.

Keep the vibes!