Type Level Option

It's common one writes code like

pub fn foo_rng<R: RngCore>(&mut self, .., mut rng: R) { .... }

#[cfg(getrandom)]
pub fn foo(&mut self, ..) { self.foo_rng(.., rand::thread_rng() }

Alone this works fine, but we could've related behavior which require sensible defaults, so the convenience methods could multiply. What tricks make this more ergonomic?

At the extreme, we'd want a builder pattern for foo invocations

pun struct Foo<A,B,..,R> {
    a: A,
    b: B,
    ...
    rng: R,
}
...
impl<A,B,..,R: RngCore> Foo<A,B,..,()> {
    pub fn add_rng(self, rng: R) -> Foo<A,B,..,R> {
        let Foo { a, b, ..., rng: () } = self;
        Foo { a, b, ..., rng }
    }
}

We'd ideally do this via a procmacro like

#[builder]
pub fn foo_rng<R: RngCore>(
    &mut self, 
    .., 
    #[builder(() => { rand::thread_rng() })] mut rng: R,
) { .... }

I've no idea if such a builder procmacro exists or what constraints it'd impose, but what about cases that lie somewhere in between? Has anyone really explored a type-level option analog that provides some? It looks rather tricky.

It's a little bit difficult to follow what you're asking for, but I guess one thing you can do if you want a builder with a bunch of default settings and a way to override them is to use default generics.

// By specifying these default generics, people can just write MyBuilder and get the defaults.
pub struct MyBuilder<R=ThreadRng, H=DefaultHasher> {
  rng: R,
  hasher: H,
}
impl MyBuilder {
  pub fn new() -> MyBuilder {
    MyBuilder {
      rng: ThreadRng::new(),
      hasher: DefaultHasher::new(),
    }
  }
}
impl<R: RngCore, H: Hasher> MyBuilder<R, H> {
  pub fn set_rng<R2: RngCore>(self, rng: R2) -> MyBuilder<R2, H> {
    MyBuilder {
      rng: rng,
      hasher: self.hasher,
    }
  }
  pub fn set_hasher<H2: Hasher>(self, hasher: H2) -> MyBuilder<R, H2> {
    MyBuilder {
      rng: self.rng,
      hasher: hasher,
    }
  }
  pub fn build(self) -> Whatever {
    todo!()
  }
}

Then, someone can use my builder without really having to write a bunch of type-level code themselves.

// This uses the default RNG, but a custom hasher.
let whatever = MyBuilder::new()
  .set_hasher(FnvHasher::new())
  .build();

The true Type-Level Equivalent to Option is two types that implement a trait you define, one of which is a wrapper. Like this:

trait TypeLevelOption<T> {
  fn into_inner(self) -> Option<T>;
}
struct TypeLevelSome<T> {
  t: T,
}
impl<T> TypeLevelOption<T> for TypeLevelSome<T> {
  fn into_inner(self) -> Option<T> {
    Some(self.t)
  }
}
struct TypeLevelNone;
impl<T> TypeLevelOption<T> for TypeLevelNone {
  fn into_inner(self) -> Option<T> {
   None
  }
}

And you could have your builder attach a TypeLevelNone by default, with a function to upgrade to a TypeLevelSome.

The other alternative is to avoid using type-level "option" in favor of using wrapper structs, so instead of MyType<FooRng, FooHasher>, you instead use WithHasher<FooHasher, WithRng<FooRng, MyType>>. This is how Diesel tends to do things, with Query implementations wrapping other Query implementations to allow building up arbitrarily-complex queries that are statically checked.

... hope that helps.

1 Like

If instead you want to ensure that everything is manually set up by the user, you just need a small modification:

// The defaults aren't good enough; they need to be replaced by the used before
// calling build()
pub struct MyBuilder<R=(), H=()> {
  rng: R,
  hasher: H,
}
impl MyBuilder {
  pub fn new() -> MyBuilder {
    MyBuilder {
      rng: (),
      hasher: (),
    }
  }
}

impl<R, H> MyBuilder<R, H> {
  pub fn set_rng<R2: RngCore>(self, rng: R2) -> MyBuilder<R2, H> {
    MyBuilder {
      rng: rng,
      hasher: self.hasher,
    }
  }
  pub fn set_hasher<H2: Hasher>(self, hasher: H2) -> MyBuilder<R, H2> {
    MyBuilder {
      rng: self.rng,
      hasher: hasher,
    }
  }

  pub fn build(self) -> Whatever
  where R: RngCore, H:Hasher
  {
    todo!()
  }
}
1 Like