Hacker News new | ask | show | jobs
by wmccullough 2854 days ago
Serious question about this line:

"Reusing code via inheritance is fragile. Inheritance also couples interfaces to implementations, which makes reuse more difficult. This is its own topic, but even OO programmers will tell you to prefer “composition over inheritance”."

Isn't the goal to have implementations of interfaces so that you can inject implementations around in order to reduce tight coupling? Implementing an interface is not the same thing as inheritance. It's adhering an implementation to a contract. Are interfaces in ObjC different than what I'm used to from Java, C#, and others?

I mean this with sincerity, what am I missing? Is this a short-coming of my knowledge because I come from a strict OOP background? I've always used implementations of multiple interfaces to achieve composition.

6 comments

A "protocol" in swift is equivalent to an "interface" in Java.

"protocol" seems to be the older name (per one of the guys that wrote Java):

> I'm pretty sure that Java's'interface' is a direct rip-off of Obj-C's 'protocol'

   -- https://cs.gmu.edu/~sean/stuff/java-objc.html
So "protocol oriented programming" is just advocating the use of swift's equivalent of java interfaces. (It'd be "interface oriented programming" in Java.)

Edit: apologies if you know all this already, it just sounded like you were asking "what makes this protocol stuff better than interfaces".

> "protocol" seems to be the older name (per one of the guys that wrote Java)

Quite likelym as another Guy (Steele) responsible for Java was also a big name in the Lisp world, including chairing the committee that wrote ANSI standard for Common Lisp. In the Lisp world, protocols were a known concept. In CL nomenclature, a protocol is a set of generic functions and types used by those functions. Which is kind of like a Java interfaces, except with multiple dispach based on any of the arguments instead of the typical single dispatch of Java and friends.

For example from TFA, a CL protocol would be a #'draw generic function and a 'drawable and 'renderer abstract types. The particular #'draw methods could be specialized on either argument (or both), and we could also imagine another protocol consisting of (the same) renderer type and some geometry-related generic functions.

Point being, protocols are both an old and very useful concept, and like usual, C++/Java-like languages only allow expressing a small subset of it.

The term from Objective-C came directly from Smalltalk, which had almost directly the same concept. Really, Objective-C is essentially "what if the bodies of Smalltalk message implementations were coded in a low-level language?".

I am fascinated, though, by your claim that Lisp also used this term... before reading it I would have been 100% sure that it didn't. I just spent ten minutes trying to find documentation of this, and can't find it... but I am not a native speaker of Lisp and so don't always know where to look; can you point me at a reference? (FWIW, I know of the meta-object protocol, which afaik is a singular thing at a different level of abstraction, and I know about classes and methods and generic functions and mulmethods, which seem like what you are talking about but aren't described by protocol?)

Common Lisp doesn't use this term at the language level - nor does any other Lisp I know, except Clojure. But it shows up around the language. Like, the Metaobject Protocol. Or CLIM - the Common Lisp Interface Manager - has a whole large spec defined mostly in terms of protocols[0].

Where do protocols originate from, I don't know. Maybe Smalltalk. But I first met them in Common Lisp ecosystem.

--

[0] - http://bauhh.dyndns.org:8000/clim-spec/2-5.html#_23

Oh yes, I know. I was part of the effort of getting that codebase running on modern CL implementations.

https://github.com/dkochmanski/clim-tos/graphs/contributors

Two lessons I learned:

One, Common Lisp code is surprisingly stable over time, with 90% of it still working on CCL/SBCL even though the codebase predates the language standard and is older than I am (don't be mislead by that 1991 on Github, it's much older).

Two, oh my god, trying to understand a large codebase seriously abusing :before/:after/:around methods in large class graphs is not an easy task, though arguably it's more of an issue with available tooling. With better ways to explore runtime program state, it would be much easier to understand and improve such code. I may have written about this last night here: https://mastodon.technology/@temporal/100646861775747986.

Also fun fact, this project is the only case where macroexpanding code crashed my SBCL...

Anyway, let that codebase be and serve as a historical reminder; for more modern implementation of CLIM standard, I'll direct everyone to https://common-lisp.net/project/mcclim/.

The only place i can think of is Norvig's slides on design patterns - http://norvig.com/design-patterns/
Equivalent, but not exactly the same, mostly in the sense that Java is less dynamic than Swift. Ones I know of:

- In Java, you have to declare the interfaces that a class implements when you declare the class. In Swift, you can add conformance of a class to a protocol, even if you don’t have access to the class’s source code.

- In Swift, protocol definitions can contain implementations of methods (I think this is being or has already been added to Java recently). For example, a method foo that takes a string argument could call the foo overload taking a character for each character in the string. That way, classes conforming to the protocol need not define the method taking a string.

- Even if the protocol definition doesn’t declare that default implementation, you can add one by writing an extension method.

All of those are useful, but also can make it hard to understand what code exactly is being called.

(More info at https://docs.swift.org/swift-book/LanguageGuide/Protocols.ht...)

> In Swift, protocol definitions can contain implementations of methods

Technically, no, they can't. What you do is write your protocol as usual, then extend the protocol with a default implementation.

In my experience, a practice that requires training to implement rather than enforcement by the language itself will become messy. Every OOP server side project that I’ve dealt with in the last 16 years eventually becomes an entangled mess. That’s across Java, Python, Ruby, PHP and C#.

Probably the biggest advantage Ruby has over the rest is the easy of monkey patching far up the inheritance tree where needed rather than having to work around everything below.

All that said, functional/compositional approaches avoid these issues at the language level by default. They are a huge win for maintenance long term.

It’s one of the big reason I got interested in Go and Elixir in the first place.

Thank you for the explanation!

If someone such as myself wanted to understand the philosophy of functional languages, do you have any book you'd recommend? Learning the languages is easy enough, but if I ever tried them I'd want to do it using the ideals of functional programming vs. trying to contort the language to be something it isnt't.

I'm noticing a trend more and more where developers move to FP over OOP after a decade or so in their career. I feel if I don't give it a look, I could be missing an obvious lesson.

I would recommend ML for the Working Programmer (available free online on the author's site). Learning ML forced me to write in a properly functional style rather than writing "C in $LANGUAGE" (the book doesn't even mention the possibility of using mutable variables until quite late on); by the same token I'm not sure the language is that usable in the "real world" (I work in Scala these days), but the lessons from it were useful even when I was working in Python.
While Scala isn't a pure functional language there are some good resources for picking up FP related concepts with it (and it's what I recently moved over to so it's most recent for me). Scala with Cats did a pretty good job for me at describing things in relatable terms. "The Red Book" (Functional Programming in Scala) goes a bit more in depth on these concepts but has a pretty steep learning curve compared to the cats book.

Also check out anything Haskell/Eta/OCaml for server side (and probably also a bit of a steep learning curve) or if you're currently a front end developer CHECK OUT ELM NOWWWWWW. Core beginner concepts to youtube would be currying, higher order functions, and composition. Monads will come once you've mastered map/flatMap/reduce/fold chaining.

There are different extremes. Elixir is one that keeps things developer friendly and enforces everything. The new Programming Elixir book is probably a good place to start. It has constructs that definitely smooth the transition...and it’s really just a wonderful language.

Plus the runtime is excellent for server side.

I think the first sentence is key, "reusing code through inheritance" is the fragile thing that doesn't work. Pure interfaces do better, but then you're not reusing much code by inheritance. Programming to interfaces lets you reuse the code that uses the interface, but not the code that implements it. That's my take, anyhow.
More importantly, this forces you to create an explicit interface for any code you'd like to reuse between implementations.

Inheritance is fragile for a lot of reasons (see e.g., fragile base class). But the most common failure modes all have the same thing in common: the error happens because a developer fails to realize that there's an implicit interface between two things in the object heirarchy.

> there's an implicit interface between two things in the object heirarchy

I think the tendency to miss this is compounded by fuzzy thinking about class hierarchies. I've lost count of the times I've heard somebody talk about an object of a child class "talking to" an object of the parent class, when calling an inherited method. They think there's actually another object -- their mental model for inheritance is composition. Using pure interfaces helps to quash this kind of thinking.

I was responsible for a shared library amongst a bunch of teams that didn’t like each other and one greedy global contractor. The latter in particular wanted more money any time the interface changed (even when there was evidence they hadn’t consumed it yet)

What I was handed was slow as molasses and insecure by design (no surprise since I arrived at late POC stage). I spent a considerable fraction of my cleverness on that project to upend the semantics and internal logic of that module with only a handful of changes to the syntax. And most of that came down to massaging the contract and a couple method signatures.

Outlawyering that team is still one of my proudest moments, but I’ve lost less sleep after I left over that code than just about anything I’ve done.

> Implementing an interface is not the same thing as inheritance.

Correct, but typical OO languages implement interfaces via inheritance since that's the only means of extension.

The problem with inheritance is that it conflates subtyping with subclassing. These are separable concerns, so with interfaces you get proper subtyping without subclassing, but you can't typically have subclassing without subtyping which is why inheritance should generally be avoided.

"Typical OO languages" meaning statically typed? I've struggled with all the problems discussed here in C++ and C# but not in Python or Ruby.
What is the difference between subtyping and subclassing? It looks confusing to me.
Subtyping describes an abstraction with which you can substitute any implementation that conforms to that abstraction's signature (Liskov substitution). Subclassing instead typically imposes both subtyping and behavioural inheritance. So as a rough approximation:

Interface implementation = subtyping

Class inheritance = subclassing + subtyping, ie. inheriting behaviour + subtyping

An OO language where you were forced to specify subtyping by implementing interfaces, but you could separately declare some implementations you inherited from without that also applying a subtyping relationship with those types, then that would be a proper separation of concerns.

Oleg discusses the issues in further detail here: http://okmij.org/ftp/Computation/Subtyping/

Oversimplifying for this context: subclassing is reusing the implementation of a parent class, subtyping is when a type can be substituted for a (parent) class for some given functionality. It's confusing because it's not always easy to explain what constitutes "substitute", e.g. see "Liskov Substitution Principle". In some languages there might not be any class or interface relationship between subclasses - they just have the right functions that do the right things.
As I understand it, subtyping is the actual conflation of interfaces (not necessarily a syntatical `interface`, but in the logical sense of "exposed things") into one type, and subclassing is direct extension, the so-called "is a" constraint.

For example, in Rust, there is no subclassing: There is no concrete type that is also another type. But there is subtyping, that is, a concrete type can be used where a interface (in Rust, called `trait`) is required, granted it implements that interface. Additionally, external sources can implement their interfaces unto existing external types.

In C#, you have subtyping+subclassing, but only as one. That is, you can have a concrete type B that is an A, and if A implements X, then B implements X. But you can't implement Y for A unless you have access to A's source. So if you want a type that is an A but also does Y, you _have_ to subclass A as a B, and implement Y on B.

The only subtyping in Rust, in my understanding, is lifetimes with the : syntax. Traits are not subtypes.
What's the distinction you're drawing? Certainly with "impl trait", any concrete type that conforms to "Iterator<Item = i32>" seems to be a subtype of "impl Iterator<Item = i32>" in every meaningful sense (e.g. Liskov substitutability).
I’m not drawing a distinction, I’m repeating what those who know more about types than me say. Maybe those people are wrong, but if so our docs need updated!

(I think it’s because traits aren’t types, and therefore can’t be subtypes. But I’m not 100% sure and my copy of TAPL is thousands of miles away at the moment...)

Traits are subtyped, they are simply a restricted kind of value that makes them more tractable than general value subtyping.
Perhaps an example? List<String> is a subtype of List, ArrayList is a subclass?

Question marks, because I am not 100% that I have it correct, either. Hoping someone will correct. :)

The line is about implementing interfaces rather than doing "extends"-style inheritance, which effectively conflates three different things: implementing the superclass' (possibly implicit) interface, composing in a value of the superclass and delegating method implementations to it.