4 ms·
>It has this great automatic interface system! I like post hoc polymorphism -- there's nothing more frustrating than knowing that a class has all the functiona
by clusmore 10y ago
>It has this great automatic interface system!
I like post hoc polymorphism -- there's nothing more frustrating than knowing that a class has all the functionality required to meet an interface but because the author didn't declare it as such it's unusuable. Implicit interface implementation is great when you happen to implement the right method names with the right signature perhaps by luck, or you define the interface based on the implementation but this is backwards. The problem is that if you want to implement two interfaces that share a method name or you simply named a method wrong, you're out of luck.
Personally I think Haskell has the best implementation of this I've yet seen in a programming language.
1. Types (structs) are defined in one place and contain only data.
2. Typeclasses (interfaces) are defined in another place and define a collection of functions (methods) and values.
3. Functions and values are defined in another place and operate on types.
4. In yet another place you can arbitrarily pair any type with any typeclass by specifying the mapping of functions/values that you've implemented with those required by the typeclass.
All four of these can be defined in separate modules, giving you the ability to allow any type (even ones you didn't define) to implement any typeclass (again, even ones you didn't define) using any implementation (even ones you didn't define) -- the implementation definition is separate from all three. Because the functions/values in tyepclasses are namespaced, there's no fear of name collision. The implementation of a typeclass by a type creates a static vtable in the compiler only, so there's no runtime penalty or dynamic dispatch. I would kill for this level of flexibility in an OOP language.
- dllthomas 10y agoI agree that Haskell currently does it best (along with Rust, which IIRC does it identically). That said, there are two ways in which the situation is not perfect. First, the dynamics you describe mean I can always add (which is great! that's usually what I need to do to get my job done). They don't help much in reorganizing. If I come out with a new package that introduces a typeclass, I'll usually provide instances for common types. If, down the line, we want to move those instances next to their definitions, both my library and theirs need breaking changes. Second, related, if I want to provide instances for types that are provided by heavy-weight dependencies, other packages that might want to depend on me would have to depend on those dependencies. Mutatis mutandis for if I am introducing the types and the libraries have the typeclasses. Peeling instances out into separate packages means a multiplication of packages or retains a coupling of dependencies, and raises maintainability concerns around orphan instances. IIRC, Backpack might fix this.
- tiglionabbit 10y agoHow does it handle the case where different packages provide conflicting definitions for any of these things?
- dllthomas 10y agoThis can break things (at compile time, unless you errantly mark something with an OVERLAPS pragma), which is why it's recommended that instances live where the type is defined or where the typeclass is defined. Instances defined elsewhere are known as "orphan instances", and trigger a warning (which you can disable) in GHC. Edited to add: Note that the above applies mostly to libraries. There's not really a downside to defining orphan instances in an application so long as you guarantee consistency (and the best way to do that, IME, is to put all your orphans in one place, with only that file marked -fno-warn-orphans).
- clusmore 10y agoWhat exactly do you mean by conflicting? Remember that the types, typeclasses and function implementations are all namespaced, so name collisions here are fine. The only problem occurs when you define multiple instances for a type and a typeclass (e.g. Int is a Monoid as a sum and separately Int is a Monoid as a product) and you attempt to import both instances into the same module, called Overlapping Instances. But if the usage of the overlapping instances is not ambiguous it wont' cause any problems. Even the following is perfectly fine: -- sum.hs module Sum where instance Monoid Int where mappend = (+) mempty = 0 ten :: Int ten = mconcat [1..4] -- product.hs module Product where instance Monoid Int where mappend = (*) mempty = 1 twentyfour :: Int twentyfour = mconcat [1..4] -- usage.hs import Sum import Product twohundredforty :: Int twohundredforty = ten * twentyfour
- deleted 10y ago[deleted]