I'm struggling with how I might implement something in Rust that is pretty straightforward in, say, TypeScript. Essentially a sum type of product types that enforces business rules at compile-time.
Suppose I have a struct Location
that holds two properties, state
and city
, where state: StateEnum
and city: String
.
Generally, city
and state
can hold any StateEnum
/String
, respectively. So far, so good:
struct Location {
state: StateEnum,
city: String,
}
However, there are business rules that make it so certain values of state
restrict the validity of certain city
. Obviously you cannot have a valid Location { state: StateEnum::TX, city: "New York City }
.
In OOP you'd make city
private and have setCity
check and throw an exception if the city wasn't legal for that state to enforce the business rule. A runtime exception.
In FP you have the ability to write your types so it's something like
type NYLocation = {
state: NY
city: 'Albany' | 'NYC' ...
}
type TexasLocation = { ... }
type Location = NYLocation | TexasLocation | ...
and your IDE would warn you that you cannot set TexasLocation.city to newCity: string
without first using a type guard in your code to narrow the possible values from string
to 'Houston' | 'Austin' | ...
.
Is there a way (I mean, obviously there has to be a way; is there an idiomatic way to make this compile-time in Rust instead of run-time like OOP has?
To a degree, yes. You can statically enforce a domain of discrete values with an enum, as you're already doing with states. You just have to extend that concept to cities.
You could have an enum for each state's cities and have Location
instead be an enum.
enum NewYorkCities {
Albany,
NewYorkCity,
// ...
}
enum WashingtonCities {
Seattle,
Olympia,
// ...
}
enum Location {
NewYork(NewYorkCities),
Washington(WashingtonCities),
// ...
}
This gives you the ability to prove that values not in the given domain cannot exist in the program.
Finally, you just need a way to convert each enum to and from strings. These kinds of functions can be generated by macros in the strum
crate.
Obviously this requires recompilation to alter the acceptable domain, but you'd have the same problem with the TypeScript code anyway, so I assume that's an acceptable drawback.