Let's say I have common logic that depends intimately on data members as well as a piece of abstract logic. How can I write this in rust types without rewriting the same code for each implementation?
Here's a toy example of what I might write in scala. Note that the abstract class has concrete logic that depends on both the data member name
and abstract logic formatDate()
.
abstract class Greeting(name: String) {
def greet(): Unit = {
println(s"Hello $name\nToday is ${formatDate()}.")
}
def formatDate(): String
}
class UsaGreeting(name: String) extends Greeting {
override def formatDate(): String = {
// somehow get year, month, day
s"$month/$day/$year"
}
}
class UkGreeting(name: String) extends Greeting {
override def formatDate(): String = {
// somehow get year, month, day
s"$day/$month/$year"
}
}
This is just a toy example, but my real life constraints are:
name
).struct
s continue to hold all those data members and complex methods.Here are some somewhat unsatisfactory ideas I had that could make this work in rust:
get_name()
method on the trait that every implementation would need. But this seems unnecessarily verbose and might also cause a performance hit if the getter doesn't get inlined.I'm not fully happy with these ideas, so is there a better way in rust to mix abstract logic with concrete logic that depends on data members?
As you noticed, Rust isn't built around a class taxonomy principle, so the design is usually different and you should not try to mock OO languages in Rust.
You ask a very general question but there are a lot of specific cases calling for different solutions.
Very often when you're tempted in a OO language to define what objects are with classes, you'd use traits to specify some aspects of the behaviors of structs in Rust.
In your specific case, assuming the right solution shouldn't involve parameterization or a i18n utility, I'd probably use both composition and an enum for the way to greet:
pub struct Greeting {
name: String,
greeter: Greeter;
}
impl Greeting {
pub fn greet(&self) -> String {
// use self.greeter.date_format() and self.name
}
}
pub enum Greeter {
USA,
UK,
}
impl Greeter {
fn date_format(&self) -> &'static str {
match self {
USA => ...,
UK => ...,
}
}
}
Your composite implementation just has to switch on the variant when needed.
(note that I don't write the implementation in this case because perf concerns would probably call in Rust for a different design and not a dynamically interpreted pattern, but that would bring us far from your question)
The most general solution seems to be my original 3rd bullet: instead of a trait, make a struct with a generic whose associated functions complete the functionality.
For the original toy Greeting
example Denys's answer is probably best. But a more general solution that addresses main question is:
trait Locale {
pub fn format_date() -> String;
}
pub struct Greeting<LOCALE: Locale> {
name: String,
locale: PhantomData<LOCALE>, // needed to satisfy compiler
}
impl<LOCALE: Locale> Greeting<LOCALE> {
pub fn new(name: String) {
Self {
name,
locale: PhantomData,
}
}
pub fn greet() {
format!("Hello {}\nToday is {}", self.name, LOCALE::format_date());
}
}
pub struct UsaLocale;
impl Locale for UsaLocale {
pub fn format_date() -> {
// somehow get year, month, day
format!("{}/{}/{}", month, day, year)
};
}
pub type UsaGreeting = Greeting<UsaLocale>;
pub type UkGreeting = ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With