> I'm not totally sure how to interpret this question. If I already have a type that includes all the values I want as a subset, I can restrict that type by limiting the values it can take and restricting or removing the operations on it to guarantee they never produce any of the forbidden values.
Sorry, perhaps I'm being a little obtuse. What I'm ultimately trying to get at is that sometimes its useful to have different types for the same data under different circumstances. A subset of data might benefit from a narrower type.
For example, suppose we have some CSV file that contains a bunch of patient information, including average white blood cell count and birthdate. This CSV file might contain bad data! So perhaps we type it as:
struct Patient {
avg_wbc_count: Either<f64, String>,
birthdate: Either<Date, String>,
}
This covers all our bases, but it's also somewhat annoying to work with. Perhaps we
only want to find patients with WBC counts outside a certain range, and discard those lines where the data is invalid. In which case, we could write a narrower type, and simply not parse the CSV rows that are invalid:
struct PatientWithKnownWbcCount {
avg_wbc_count: <f64>,
}
So in this case we have a couple of options for types. The latter type decouples the birthdate (it's never even parsed), but adds the restriction that the WBC count needs to be numerical; while the former type is a more accurate representation of the CSV file overall, but has greater coupling.
Obviously which we use depends on the nature of our program, but what I'm trying to get at is that a type isn't set in stone, but something we choose. Ideally we choose the most restrictive and decoupled type for the circumstances, but that might result in having many hundreds of different variations of the same type, so there's a tension between the number of distinct types and how specific or narrow they are.
> In the Rust case you need to write down these things explicitly so the compiler can make sure you don't pass a value that doesn't satisfy them. In Clojure, the reason you don't need to write them down is that every value in the language is restricted to only be able to represent such values, so those constraints are implicitly on every function that you write.
Granted, but Clojure's approach can result in greater decoupling with less effort, particularly when dealing with imperfect data.
In the previous examples I presented a scenario where we might want to think carefully about how exactly we store data that might contain invalid fields. But in Clojure we don't care - we can quite easily have a data structure that's partially invalid or unparsed - the entire problem of how we represent this data in memory is sidestepped.
We can still define what constitutes valid fields:
(s/def :patient/birthdate inst?)
(s/def :patient/avg-wbc-count float?)
But these schema definitions are independent of the type system (i.e. decoupled), so we can apply them selectively or not at all. We can say "this function requires patient data with valid birthdates and WBC counts, but we don't care about any other field".
Could you do the same in Rust or other similar languages? Sure, it's ultimately just maps and predicates. But many languages aren't geared up to make that pleasant to use.