Enforcing at compile time that a method may only be called once for each given type argument

Desired outcome:

fn foo(bar: Bar) {
     // Fine
    bar.whiz::<u32>();
     // Compile error - should only be able to call once for a given type parameter
    bar.whiz::<u32>();
    // Fine again, because we haven't called with u16 yet
    bar.whiz::<u16>();
}

This constraint should be local to an instance of Bar, so given a fresh Bar we can call them all again.

FnOnce does something similar, so is there a pattern that allows for the type system to enforce this constraint somehow? I'm curious about the limits of this, so accepting wacky proposals if there isn't a straightforward way.

I don't know whether it's possible using a normal function call, but you could probably use a macro that calls the function and adds an extra impl to some type. That way you trigger the "conflicting impl" error.

fn generic_function<T: std::fmt::Debug>(value: T) {
    println!("Calling with {:?}", value);
}

macro_rules! call_once {
    ($name:ident :: < $generic:ty >($value:expr)) => {{
        impl CalledTwice for $generic {}
        $name::<$generic>($value)
    }};
}

trait CalledTwice {}

fn main() {
    call_once!(generic_function::<u32>(42));
    call_once!(generic_function::<u32>(56));
}

(playground)

   Compiling playground v0.0.1 (/playground)
error[E0119]: conflicting implementations of trait `CalledTwice` for type `u32`
  --> src/main.rs:9:7
   |
9  |       impl CalledTwice for $generic {}
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |       |
   |       first implementation here
   |       conflicting implementation for `u32`
...
18 |     call_once!(generic_function::<u32>(56));
   |     --------------------------------------- in this macro invocation
   |
   = note: this error originates in the macro `call_once` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0119`.
error: could not compile `playground` due to previous error

Embedded devices have a similar situation where you want to make sure it's impossible for someone to access a peripheral multiple times. The way they resolve this is by making sure all peripheral code requires a token, then you have a singleton Option<Token> which you take().

This doesn't enforce things at compile time, but it seems to be a happy middleground between ergonomics (macros can be ugly and the error messages are poor) and safety (we still want to enforce the use-once invariant). In practice this isn't an issue because you'll always initialize a peripheral on startup, so any duplicate uses will trigger a crash/error handler within a couple milliseconds of booting.

5 Likes

That's clever. Dirty as all hell, probably too dirty for me to use in an actual API, but clever!

I can detect and panic on duplicate calls easily already, so I'm only really interested in compile time solutions.

Use HachMap<TypeId,Boolean>

Every type defined in rust codes has the unique id, and std::any::TypeId provides a function that allows for you to get the id of the certain while 'runtime'

Basic usage is

let a = std::any::TypeId::of::<i32>()
// TypeId { t: 13431306602944299956 }
println!("{}",format!("{:?}",a));

Then you can make a Hashmap::<TypeId_instance,Boolean> that records whether certain function is called twice.

// decleare the hashmap as a global variable
const recorder = Hashmap::<TypeId,Boolean>();

// inside bar.whiz
impl Bar {
    fn whiz<T>() {
        let id = TypeId::of::<T>();
        if !recorder.contains_key(id) {
            recorder.push(TypeId,true)
        } else {
            // do other stuff
        }
   }
}

or if you hate to mass around with the function

 
macro_rules! check_typeId {
     (... $type:ty...) => { // place if statemens here }
}

check_typeId! {
    bar.whie::<i32>();
}

std::sync::Once

'Once' struct guarantees that a closure assigned is called only one time. After calling it, the struct caches the return value of funcntion.

The example from the docs is

use std::sync::Once;

static mut VAL: usize = 0;
static INIT: Once = Once::new();

// Accessing a `static mut` is unsafe much of the time, but if we do so
// in a synchronized fashion (e.g., write once or read all) then we're
// good to go!
//
// This function will only call `expensive_computation` once, and will
// otherwise always return the value returned from the first invocation.
fn get_cached_val() -> usize {
    unsafe {
        INIT.call_once(|| {
            VAL = expensive_computation();
        });
        VAL
    }
}

fn expensive_computation() -> usize {
    // ...
}

Once has a helper function 'is_completd' which gives a boolean value that indicate whether the closure is called or not.

Since your function is a method of Bar, you can use a method function pointer or just pass a bar to get_cached_val function as a reference.

create your own runtime context

There is a crate 'Rhai' that run an embedded dynamic function within rust codes. It works like lua in C++. The way the crate implments a domain-specific-language is through making a seperated runtime context.

Remind that every functions and struct definition in rust is actually pointer. Then you can have a bunch of hashmap that contains pointers and their metadata. Let's call a top level struct as engine. It would look like this

struct Engine {
    // ...
    recorder:Hashmap
}
impl Engine {
    // ....
   fn register_fun(&self,func_name,key) {
       self.recorder.insert(key,func_name);
   }
  fn run_func(key,args) {
      let f = self.recorder.get(key);
      f(args);
  }
}

let engine = Engine();

engine.register_fun(func_name,"register_key1")
engine.register_struct(struct_name,"register_key2")

// ...
engine.run_func("register_key1",args);

let output = engine.make_struct("register_key2",args);

If you place codes that check runtime information of function in run_func it menas you can handle. Check rhai/src/engine.rs to get more inspiration.

I'm sorry! I messed up my question and forget to make explicit that I'm looking for compile time solutions! The FnOnce comparison sort of implied that, but I wasn't nearly as clear as I should have been!

I already have a solution based on TypeId and am interested in promoting it to a compile time check, if the type system is capable of that.

I'm sorry that you wasted the effort in that high quality reply to my badly phrased question! Thank you for your effort!

1 Like

FnOnce works by taking ownership of the value, so that it can't be used a second time. If you have only a few types you care about, you can do something similar by breaking Bar up into two public fields: an implementation struct that does the real work, and a zero-sized token struct that has the single-use tokens (similar to the Option<Token> that @Michael-F-Bryan mentioned).

It might look something like this, but this implementation is only advisory— The tokens aren't tied to a specific Bar instance, so you can take a token from bar2 to call the corresponding method on bar1 a second time.

pub struct Token<T>(PhantomData<T>); // field private so that construction is controlled.

pub struct BarTokens {
    pub u32: Token<u32>, // fields public so that user code can destructure this
    pub u16: Token<u16>
}

pub struct Bar {
    pub bar_impl: BarImpl,
    pub tokens: BarTokens
}

impl BarImpl {
    pub fn whiz<T>(&mut self, _token: Token<T>) {
        // ...
    }
}
1 Like

Sadly I don't have a finite set of possible types - this needs to work with any arbitrary types.

This is going to be very hard to enforce in the type system, because of situations like this:

fn f<T,U>(bar:Bar) {
    bar::whiz::<T>();
    bar::whiz::<U>();
}

Rust doesn't provide a mechanism for determining whether or not T and U are the same type (other than TypeId. You might be able to do something clever with type-level HLists, but it'll be a bit of a mess.

EDIT: Actually, that gives me an idea. Let me try something...

1 Like

This seems very much like a code smell working around action at a distance. Why do you want to enforce this kind of global constraint? You should probably redesign your API so that functions are safe to call multiple times (or even idempotent).

1 Like

If you reall want force it to compile time, then defining custom lint would work for you.

There is a crate "dylint" that expands clippy features by letting user define a specific lint test function.

Check this and crate

1 Like

I managed to get something working on nightly, but it's a huge hack (especially transmuting TypeId to u64). It at least shows that something similar might be feasible in the future:

#![feature(generic_const_exprs)]
#![feature(inline_const)]
#![feature(const_type_id)]

use std::marker::PhantomData as PhD;
use std::any::TypeId;
use std::mem::size_of;

fn main() {
    let bar: Bar<Nil> = Bar { called:PhD };
    let bar = bar.whiz::<u32>();
    let bar = bar.whiz::<u64>();
    // Uncomment the line below for a compile error
    // let bar = bar.whiz::<u32>();
}

#[derive(Copy,Clone)]
enum IdList {
    Cons(TypeId, &'static IdList),
    Nil
}

const fn cmp_type_ids(a:TypeId, b:TypeId)->bool {
    unsafe {
        let a:u64 = std::mem::transmute(a);
        let b:u64 = std::mem::transmute(b);
        a == b
    }
}

impl IdList {
    const fn contains(self, needle: TypeId)->bool {
        match self {
            IdList::Nil => false,
            IdList::Cons(h,t) => cmp_type_ids(h, needle) | t.contains(needle)
        }
    }
    
    const fn ensure_missing(self, needle:TypeId)->usize {
        if self.contains(needle) {
            panic!("Duplicate call");
        } else { 0 }
    }
}

struct Nil;
struct Cons<H,T>(PhD<(H,T)>);

trait HasIdList { const IDS:IdList; }
trait HasId { const ID:TypeId; }

impl<T:'static> HasId for T {
    const ID:TypeId = TypeId::of::<T>();
}

impl HasIdList for Nil { const IDS:IdList = IdList::Nil; }
impl<H:HasId, T:HasIdList> HasIdList for Cons<H,T> {
    const IDS:IdList = IdList::Cons(H::ID, &T::IDS);
}

struct Bar<T> {
    called:PhD<T>
}

impl<T:HasIdList> Bar<T> {
    fn whiz<U:HasId>(self)->Bar<Cons<U,T>> {
        dbg!(T::IDS.contains(U::ID));
        let _assert:usize = const { T::IDS.ensure_missing(U::ID) };
        Bar { called: PhD }
    }
}
5 Likes

Now do it with a hash map instead of a linked list for shorter compile times! :rofl:

Very cool! Evil, but awesome. I don't think I'll be using that in production code anytime soon, but transforming into a chain call type API and then building up an exclusion list in the type is a very creative solution!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.