-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Traits or no traits #31
Comments
Having your patch state error at runtime is not so bad. Ideally for these cases you would create an interface that forced a sandbox environment, for example a test function that is passed a sandbox worker. I agree with @matklad that the simpler we can keep the API the better. Others can make traits on top of this if they so desire. |
Yeah, so that's what I have going with right now with these traits. If we really want to eliminate as many traits as we can, we can get rid of
Well to be fair, the usage isn't that fine grained: we'd just be looking at There's one more requirement I think needs to fit the bill here too, but might lean into over engineering: we need to allow custom networks later (like when RPC as a service becomes a thing and would require different RPC endpoints or if a user wants their own separate RPC service). That's where I think these traits come in handy, since we can now generalize over a set of types/networks instead of requiring us to hardcode the concrete type itself. Alternatively, we could have a trait object underneath but we then lose specific network info (unless we want to introduce downcasting to the mix...). Overall, I think that requiring a type parameter for Also, one thing to note is that there is differing behavior between how sandbox vs testnet/mainnet does top level account creation, so we do need an interface around that. I'm sad we can't go about it in a more config/data-driven way, but that's also one of the reasons why I went with traits originally. |
Not exactly: the primary angle I am looking from is "are we forcing the user's code to be generic at compile time, or polymorphic at runtime"? The number of traits doesn't matter pre se, it's the fact whether worker has a type parameter or not, and, consequently, whether
Hm, but why we need custom types for each network, rather than custom runtime config? Why something like
Yeah, we definitely need to implement different behavior here, but it seems like an implementation concern here, rather than than API concern. Ie, if we just want to have different impl for test net and sandbox, but do want to use traits internally, we can do something like the following: // NB: the only pub thing
pub struct Worker { repr: WorkerRepr }
enum WorkerRepr {
Sandbox(Sandbox),
Testnet(TestNet)
}
trait TopLevelAccountCreator {}
impl TopLevelAccountCreator for Testnet {}
impl TopLevelAccountCreator for Sandbox {} |
ahh got it. Thought you wanted to eliminate the amount of traits since they were excessive in number at first but maybe I'm mixing up comments. For the runtime, we can do the following in its current state, but requires some know-how about rust and dispatching: let worker: Worker<Box<dyn Network>> = if condition {
workspaces::sandbox().into() // or some other more intuitive conversion function name.
} else {
workspace::testnet().into()
};
Yeah, this was the part I was saying about maybe leaning into over-engineering. I can see we give the ability to users to do their own custom network with custom behavior like
huh I see, that might just work I think. Additionally, we can still have network specific behavior by casting/matching it to the variant:
I wonder how the custom network I mentioned before would fit into the enum. Maybe something like:
|
Yeah, that's what I'd go for for an extensible interface. This is a blend of two patterns:
In a nutshell, most abstractions have two sides -- user interface intended for the consumers that use abstraction, and customization interface intended to provide several implementations of the abstraction. The two needs are often pretty orthogonal, but sometimes the same language mechanism is used to implement them both. The (anti-)pattern here is to accidentally mix the two. The pattern is to make the two separate. You last suggestion is exactly this. /// This is fully concrete type for users of the worker.
pub struct Worker {
repr: Box<dyn WorkerTweaks>
}
/// This is a customization point
pub trait WorkerTweaks { ... }
impl Worker {
/// This is the magic: we take user-supplied customizations, and wrap them in concrete types.
pub fn with_tweaks<T: WorkerTweaks>(t: T) { Worker { repd: Box::new(t) } }
} But all this is basically generic philosophy :) To get more concrete, we can make the interface to the library generic or concrete, with the primary tradeoffs being type-safety vs ease of use. I feel moderately strongly that concrete would be a better interface here, but I don't have enough context to make a judgement call here. |
mmm, poor man's abstract classes here we go... I'm only joking about this one
So what you're saying is that you prefer the enum variants + trait object vs purely just trait object right? I think as long as we don't expose those parts, and just have a good enough casting/matching interface or some runtime checks/guards, we can change it up later if it needs to be. I can just start out with the former and see how it goes |
Not exactly: I don't care at all what we use internally. Maybe variants are better, maybe the trait object, but this doesn't really matter, as we can change from one to another and back at a whim, as that doesn't affect public API at all. What I feel strongly about is just the public interface we expose: is it |
Makes sense. Let's try with purely
What would've been interesting is if we could've done: impl Network for Box<dyn Network> {}
struct Worker<T: Network = Box<dyn Network>> but seems like it doesn't fully work for nested types or whatever it's called: fn do_something(worker: Worker) { ... }
fn main() {
let worker: Worker<Box<Sandbox>> = Worker::sandbox();
do_something(worker); // compile error
} even though it compiles for this case: fn do_something(workspace: Box<dyn Network) { ... }
fn main() {
do_something(Box::new(Sandbox));
} I think it just requires some further casting but might be pretty hacky underneath. Plus, we'd explicitly write |
That's plausible, and that's what aws lambda does: https://docs.rs/aws-sdk-lambda/latest/aws_sdk_lambda/client/struct.Client.html You can think of |
There are two alternative ways we can design the overall shape API.
One is to make the Worker type to be parametrized by network type. So things like
Sandbox
andTestNet
are different types. The public API would look roughly like this:The usage would look like this:
An alternative is to not make type-level distinction between the types of network. The public API would then look like so:
The usage would look like this:
The question is, which one should we choose.
Traits:
Pros:
Cons:
asyc_trait
, which is cool, but is fundamentally a hack.No Traits:
Pros:
runtime_generic_usage
)Cons:
Worker::new_mainnet().patch_state()
and get an error at runtime, rather than at compile time.The text was updated successfully, but these errors were encountered: