Are there design patterns for rust, for example object safe traits?

I am a Rust beginner. I would love one day to be able to write safe code with Rust. I can see all the rationales for it to exist, and its goals, and I 100% subscribe. After having moved along quite a set of different languages, I said to myself that I wouldn't learn any new language anymore. I think Rust deserves the exception.

Anyway.

There are things which I struggle with quite a bit. For example:

pub trait PrivateKey {
   fn generate(&self) -> Self;
}

pub struct Node {
   pk: Box<dyn PrivateKey>,
}

This doesn't compile because of object safety. I understand why that is needed. But then I struggle to design around such issues.

  1. It's probably legit to want different kind of PrivateKey implementations, as there already exist different ones, and new ones may come along.
  2. I want my node to have a reference to a private key, as it needs to use it in many use cases.

So I want to define in one place what the PrivateKey implementation is, and then be able to use it. I want to instantiate it (hence no new function in the trait), but then be able to generate the actual bytes and returning an instance of itself. This way, I'd be able to use the same abstracted code in one place to generate a new private key and then use it.

But an object-safe trait can not return Self. So how do I design around such issues? Are there general patterns, or (as in fact it looks to me), do I need to look for custom solutions every time?

If you can live with always boxing and erasing the PrivateKey, you could have the trait fn return the boxed/erased type:

        pub trait PrivateKey {
            fn generate(&self) -> Box<dyn PrivateKey>;
        }

(I assume you mean pub struct Node btw, not a trait there.)

3 Likes

Oh yes, sorry, it's a struct obviously. Will edit the post.

What do you mean by erasing?

That's what a dyn SomeTrait is called, since the type implementing that trait is lost/erased.

Does the trait fn I described work for you?

1 Like

The signature is the same a Clone::clone. Maybe you're looking for a way to clone Box<dyn Trait>.


Coercing from struct ImplementorOfTrait to dyn Trait (like in a Box<dyn Trait> is also called "type erasure".

If instead of cloning Box<dyn Trait> you wanted to get the base type back out, that's where downcasting would come in. Or maybe you should rethink the idea of using trait objects if that's the case -- it's less common in Rust to type erase and attempt downcasts all over the place. Maybe you want to make Node generic with a PrivateKey + Clone bound where needed instead. Or maybe use an enum[1] instead, if you only need a handful of local implementations.


  1. marked as non_exhaustive so you can extend it later ↩︎

4 Likes

Yeah, it does work for me. I can't assess if it's the best way to do it for my use case, but for the time being it works! I guess I can mark it as the solution.

So I have actually received multiple responses to my concrete implementation problem. However, I yet have to learn how to find the best solutions myself, and also to evaluate what's best for my use case.

The tutorials and pages I have seen so far though, teach the basic language features, but, as a somewhat experienced long-term programmer where more complex scenarios arise, I often find myself at a loss trying to identify best-practices for more complex challenges.

So to my other question - are there any design patterns, or any educational resources which teach this kind of things? Or is it rather you learn as you go, by applying solutions to the problem at hand and thus learning step by step? I am not sure if my question makes sense...

You may want to think about what @quinedot said about the signature of generate. It is strange that it has a &self parameter since this means it is creating one PrivateKey from another PrivateKey. This makes one wonder whether you really need this method in your trait, and if there may be a misunderstanding. Are you trying to create a trait for a PrivateKey factory of some kind?

3 Likes

My underlying assumption is that no matter what implementation, a private key needs to be generated somehow. This step however is often "externalized", in the sense that it is just assumed that the private key is being provided - however, often in some kind of hard-coded type. But if it is provided, I am not sure I can convert it easily to my trait? That's why I was wanting to include its actual generation in my trait too.

I need to think about if I really need to generate the private key myself, or if there is a way to get a custom PrivateKey trait implementor from an arbitrary cryptographic private key implementation.

If what you want is a trait of your own for private keys, you can always wrap the generated key in a struct that implements the trait. This is common.

But I think you're on the right track when you say that you first need to figure out how to generate them.

2 Likes

My favorite design pattern in Rust is: Avoid dyn traits where possible. Is there a specific reason the PrivateKey should be a trait, not a struct or enum?

3 Likes

Off-topic warning:

In my opinion, this is the very definition of learning—trying things, failing, and then learning from those experiences. The more often you fail, the more experience you gather. Memorization, of course, is also a part of the overall process.

1 Like

I have tried to explain my reason earlier up.

There are a number of different cryptographic libraries which provide public/private keys. Usually you have to settle for one and more or less hard-code it. However, how to use them is basically always the same: serialize to disk, deserialize from disk, convert from/to bytes, and sign something with them.

I am not cryptographically versed enough to assess if new implementations will show up in the near future, however, sure enough, crypto is in a lot of movement, so it's possible.

Therefore I wanted to abstract away the functionality of a private key, so that I could use any implementation, especially thinking about a potential new upcoming implementation in a month, a year or two.

You can do all this stuff with dyn PrivateKey, assuming it has an appropriate API.

But you still have to have a private key to do all of those things.

(actually, deserialization)

Deserialization is an exception... but the devil is in the details. It is possible (as far as the type system is concerned) to implement Deserialize for something like Box<dyn PrivateKey>. But there's no general way to write the deserializing code except by enumerating all the known possibilities - how else would you do it? So that case isn't really supported by type erasure, either, but this isn't a type system or language limitation, it's a matter of defining exactly what you want "deserializing an erased type" to mean and then writing the code to do that.

So your problem of creating a private key isn't really addressed by what you say is the same between private keys.

Sure. But what will determine what implementation actually is used? You haven't answered that question yet. Why shouldn't the code that actually answers that question be the code that creates the object (and passes / returns it to the other code, presumably erased)?

3 Likes

There we go. In idiomatic Rust code dyn Trait is the least favorable way to abstract away the functionality.

Use enum if you want to use a value which is one of several variations. Those variations should be known when you wrote the code, but you can always add new variations by writing more code. Sometimes you wrap the enum within a struct to represent common fields across variations, or just want to hide the enum from the outside.

Use generic code if you want to use specific types known at compile time, but want to reduce repeated common code by abstraction. Define common functionalities as traits and write common code that only utilizes said abstract functionalities. If you write libraries you may expose the trait to allow users to utilize your common code with their own types by implementing the traits.

Use dyn Trait if you want to use value whose type is not known when you write the code but it's known the underlying type of the value implements certain trait. Usually it happens when you write frameworks. With frameworks users may extend existing functionalities of it by inserting their own types into the loop.

Honestly, you rarely write frameworks and especially not as your beginner project. It's not late to consider dyn traits after you realize all the other approaches doesn't make any sense.

2 Likes

Thanks everyone. This thread has been very helpful for me. First of all, I realized that more often than not, we start writing code to get something up and running quickly, and then improve over time.

It seems to me Rust doesn't "support" that kind of approach, as it forces you to think deeper about what you actually want to do. It somehow discourages quick (and dirty), but not because you can't do it, but because you usually have to think more and more concretely about your code. Which I think is good - at the expense of the usual quickie just to get something running. I am sure though that over time, you acquire also the skills to do this upfront thinking faster too.

Thanks especially to @Hyeonu , your last post was exactly the type of general design thinking I was looking for.

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.