I do believe there are cases where dynamic typing is genuinely useful, but those make up a very small portion of real-world problems.
I do believe there are cases where dynamic typing is genuinely useful, but those make up a very small portion of real-world problems.
A couple of years ago I worked for DARPA project that was building a system designed for greatly enhanced security and reliability. It had a novel programming language designed to be able to express a rich set of reliability and security constraints in the type system. It had a rich static type system, but it also had a dynamic type system. Why? Because some important constraints simply can't be analyzed statically.
For example, a program could say that it was impossible for a given piece of information to be delivered to an unauthorized receiver. That constraint cannot be analyzed statically because the authorization status of a given agent can change at any time. The type system had to be able to check authorization, so it had to have a dynamic checker (in fact, the system was designed to be able to do dynamic checks in hardware).
Maybe this sounds like an esoteric use-case, but is that because nobody wants to be able to express such constraints, or is it because we just don't expect to be able to do so because we haven't had the tools?
The project in question is in part meant to make the case that such use-cases should not be esoteric--that one of the reasons that our reliability and security situations are so embarrassing is that we haven't so far taken those considerations seriously enough to develop the tools to deal with them. If we want to get better, maybe we should be thinking about how to integrate both static and dynamic type systems, rather than exalting one and denigrating the other.
On the other hand, I disagree with the scenario you describe: if you have multiple types that come from an external, closed-source library, you won't be able to have them extend a new interface. If you're lucky and they've not been marked final, you'll need to wrap them in another type that extends the right interface, then unwrap them... not at all impossible, but a lot of boilerplate.
The best solution I know of is type classes, in languages that support them.
If you have a function that expects a type A, and you want to pass an object of type B that doesn't have A as a supertype, I don't know of many ways to do so. Either you wrap your B in a type that extends A, or you create a new implementation of B that extends A (this is often the same solution as the first one), or you use type classes or some variant of the concept. Is there an other option that I'm not seeing?
They allow you do something else, something I would argue is better (and I explicitly said so, "The best solution I know of is type classes, in languages that support them."), but are entirely irrelevant to sanderjd's point and my answer.
But, yes. The more I code, the more it feels like type classes are never not a good answer.
"The more I code, the more it feels like type classes are never not a good answer."
As they are used in Haskell, they sometimes get awkward where you where multiple instances are relevant to the same data in the same scope. You can newtype cast around, but I've been wondering (with multiparameter typeclasses) if reifying the instance we care about is 1) useful, and 2) actually still distinct from explicitly passing the dictionary as a record. Regarding 1, I've consistently leaned "yes", although without tremendous confidence. Regarding 2, I've gone back and forth over time.
The way I understood that is, if you have a f(a: A), and need to pass it a B and C, neither of which are subtypes of A, you can just create a new type D, have A, B and C extend D, and modify f to expect a D. My point was that it's not always practical nor even possible to do so.
A type class would be a good way to abstract over the type itself and simply require a specific behaviour, but by the time you have f(a: A) where A is a concrete type, and you can't modify f (because it comes from an external library, say), it's too late to retrofit type classes into it.
As for your other point - type classes being awkward when you have multiple possible instances for a given type - I agree, but must say that I'm spoiled: my primary use of type classes is with Scala, where they're just, when you come down to it, parameters to functions. You can let the compiler infer them when there is no ambiguity, but when you need to pass an int to a function that expects "A with a monoid", you always have the option of being explicit about which monoid instance to use. I believe Haskell treats them as implicit parameters as well but without the option to pass them explicitly.
Finally, I'm afraid you've lost me with your last bit - reifying the instance we care about and passing the dictionary as a record. I wish I had something smart to answer, but I'm not sure I actually understand what it means. I know enough Haskell to be able to be able to read it (a necessary skill if you have even the slightest interest in FP), but not much more than that. Maybe if I knew more I'd have a clearer understanding of what you mean?
I agree with your starting point, but to my mind type classes are precisely a way to "make [types] implement an interface". I completely agree that it is often impractical to do so by defining new types.
I'll address the rest when I have a little more bandwidth - I hope you'll forgive me; between interesting projects at work, a new baby, and holiday goings on, attention is in high demand.
I understand that we're not arguing, and find the conversation interesting: I approach it from the point of view of someone who was mostly taught OO (and learned whatever CS theory he knows by himself), where you seem to have a more solid FP and theoretical background and entirely different viewpoint.
As a new father myself, I absolutely understand that free time is scarce. Enjoy your baby and holidays!
"Passing the dictionary as a record" is a pretty common alternative to typeclasses. The way it works is if you have a typeclass:
class Foo a where
foo :: ...
bar :: ...
you instead make a record that looks like: data Foo = Foo { foo :: ..., bar :: ... }
This works great for a lot of things, and is often preferable to typeclasses when all you're doing is defining an interface. One objection is that things look slightly less tidy, but the main problem with it (where it's a problem at all) is that there's no way of identifying individual implementors of an interface by type anymore. An example where this relevant is Set. We could define the Set operations to take a comparison function, but we couldn't be sure that all comparison functions agree across all interactions with a particular set.What I mean by "reifying the instance" is indexing typeclasses with a (single constructor) type that identifies the instance, and then passing that around to let us select which instance we care about, even for the same underlying type.
For example:
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
import Data.Function (on)
class ParamOrd o a | o -> a where
pcompare :: o -> a -> a -> Ordering
data StringsOverLength = StringsOverLength
data StringsLexicographically = StringsLexicographically
data IntegersInOrder = IntegersInOrder
data IntegersReversed = IntegersReversed
data ReversedOrdering r = ReversedOrdering r
instance ParamOrd IntegersInOrder Integer where
pcompare _ = compare
instance ParamOrd (ReversedOrdering r) Integer where
pcompare o = flip (pcompare o)
instance ParamOrd StringsOverLength String where
pcompare _ = compare `on` length
instance ParamOrd StringsLexicographically String where
pcompare _ = compare
Now we could write a new version of Set, `ParamSet o a` such that we can still enforce agreement while choosing our orderings with an argument rather than a newtype.I went around in circles and ultimately concluded no amount of type inference or other things in Scala's bag of tricks would let a user of the API avoid the dreaded type cast.
I have often seen this situation whenever I have tried to write generic code in a static language that needs to interact with a database.