heya, I'm designing an API for consumers to implement, and wondering which API is more digestable.
- Consumers have to implement a state discovery function, but depending on the usage, the
src
may or may not exist. - In a "read" usage,
src
not existing is okay. - In a "write" usage,
src
not existing is an error.
What are your thoughts on the following designs? (pseudo rust)
-
Two functions, whose return type accurately depicts the concept.
trait ItemSpec { type State; // if !params.src().exists() { Ok(None) } fn discover_for_read(ctx: &Ctx, params: &Params) -> Result<Option<Self::State>, Error>; // if !params.src().exists() { Err(..) } // // Pros: this doesn't allow the consumer to return `Ok(None)` // as it conceptually doesn't fit the execution model. fn discover_for_write(ctx: &Ctx, params: &Params) -> Result<Self::State, Error>; }
-
One function, whose return value should be correctly chosen by the consumer.
trait ItemSpec { type State; // if !params.src().exists() { // if ctx.read() { // Ok(None) // } else if ctx.write() { // Err(..) // } // } // // Pros: less things to think about in the trait fn discover(ctx: &Ctx, params: &Params) -> Result<Option<Self::State>, Error>; }
Notably this is introducing an opportunity to represent invalid state in the return type.
An evolution of the question, instead of each function within the Spec
being just a function, they are their own separate trait:
// instead of
fn discover_for_read(ctx: &Ctx, params: &Params) -> Result<Option<Self::Output>, Error>;
// it becomes
trait FnSpec {
type Output;
fn exec(ctx: &Ctx, params: &Params) -> Result<Self::Output, Error>;
}
So the umbrella trait is now either:
trait ItemSpec {
type State;
type StateDiscoverReadFn: FnSpec<Output = Option<Self::State>>;
type StateDiscoverWriteFn: FnSpec<Output = Self::State>;
}
or
trait ItemSpec {
type State;
type StateDiscoverFn: FnSpec<Output = Option<Self::State>>;
}
Given this abstraction, what would your bias be?