> Let's make sure we agree on a definition of "intuitive". I am referring to the probability that an average developer will grasp the subtyping concept and understand it instinctively once explained to the point they will easily do it correctly from that point forward.
Yes, I mean that too.
> I am not referring to the inherent beauty of the concept.
Me neither.
> By that definition, my argument is that proper subtyping is counterintuitive to an average developer.
I disagree. People aren't stupid, and are able to grasp basic distinctions once they're pointed out, especially when they're newcomers to a topic. On the other hand, once they have some experience, it's harder for them to get rid of their misconceptions.
> Your statement that it's intuitively obvious strikes me as an opinion about the concept itself, rather than the ability of average developers to grasp it.
Contravariance of argument types and covariance of return types can be explained in 5 minutes to anyone with basic logical reasoning skills. People who lack such skills have no business being programmers.
> It is wrong, and yet it is very common for developers to have that wrong intuition.
That's because they've been lied to since they were beginners. It's not true that:
(0) Subclassing and subtyping are the same thing.
(1) You can understand the semantics of programming language features (such as subclasses and subtypes) in terms of natural language analogies.
> (and trying to make a pediatrician a subtype of doctor)
Trying to make Pediatrician a subtype of Doctor is correct. What is wrong is pretending that every Doctor can treat any Patient. Unfortunately, implicit subtyping in the presence of abstract types (not the same thing as abstract classes!) is rather tricky, but, assuming you don't mind upcasting manually (which is perfectly safe, albeit tedious), this is readily expressible in Standard ML:
signature MEDICAL_ATTENTION =
sig
type doctor
type patient
(* no result is passed to the continuation,
* we only run this for its side effects *)
val treat : doctor * patient -> unit
end
structure Pediatrics :> MEDICAL_ATTENTION
where type patient = child =
struct
type doctor = ...
type patient = child (* assumed defined elsewhere *)
fun treat (d, p) = ...
end
structure Cardiology :> MEDICAL_ATTENTION
where type patient = person =
struct
type doctor = ...
type patient = person (* assumed defined elsewhere *)
fun treat (d, p) = ...
end
val child_as_person : child -> person
As can be seen,
some doctors (e.g., `Cardiology.doctor`s) can treat any person, but not all.