"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.
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.
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.
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.
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.
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.
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.
> 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.
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.
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...)
Ok, I guess, this only works with trait objects or generics, so traits themselves aren't types in the formal sense (as suggested by others. I am not a type system expert nor do I have formal CS theory education).
As I understand (which again, might be flawed, please correct me), it would be more better to say that a type parameter bounded by a trait is a type, and a type implementing that trait is a subtype of the parameter?
But trait objects have types, and such can be subtyped, correct? That is, I can use a `&Bar` where a `&TraitFoo` is expected if `impl TraitFoo for Bar`.
I think the point is that `&TraitFoo` would never be expected - that there was no way to have that be the type of a term in the first place? I don't think that can reasonably be said to be true in the days of impl trait though.
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:
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.
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.
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".
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.
> "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.
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 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.
The idea proposed: use protocols and value types. This avoids the problems with subclasses and with mutability (concurrent modification). Enums are also used as a controllable way to have different kinds of related objects and use a switch statement to distinguish them.
It is fun to see how the benefits of immutability, typeclasses, and (sort of) ADTs / pattern matching are being discovered independently.
Was about to comment in a similar fashion. One more example of a language who has embraced this: Clojure
Protocols, polymorphism, dynamic dispatch (i.e. multi-methods) and immutability baked in.
It’s a compromise between structural and nominal typing to boost code reuse without locking yourself prematurely into structural typing. It’s been tried before and has been given different names.
Personally I’m fond of it. It’s one of several ways to blur the line between “is a” and “has a” relationships. We used to go out of our way to distinguish the two (eg in UML) but it has fallen out of favor.
A coworker long ago convinced me that improvements to delegation would be coming soon to new languages and unfortunately this is one of the first times I’ve seen it happen close to the way he hoped.
In theory: fine. In practice: it's Swift. I would think very carefully before using a bunch of enums with payloads (aka " associated values"). The current syntax means you will probably wind up with either unreadable code, or a lot of extra complexity. I hope in the future, Swift improves the syntax for comparing, creating payload enums, and accessing their payloads. Until then, there are pros, but large cons too.
It sounds closer to Haskell to me. ADTs are very similar to associated values that Swift has, although I think they're on a slightly higher type level. I haven't found Haskell too unreadable but that's probably because it ditched C-style syntax whereas Swift stuck with it. Maybe it's a trade-off worth making though, since sticking with familiar syntax lowers the bar.
I don't know how Haskell does it, but, at present, Swift enums with payloads have caveats that simple Swift enums don't:
- they use this verbose syntax to access payloads "if case let .foo(bar) = baz { /* do something with bar */ }" The alternative being to do everything in a switch statement, and possibly create a local variable thanks to scope
- their payloads are not convertible to tuples, despite their cosmetic similarity
- they do not allow default values, despite their cosmetic similarity to swift function parameters (which do support default values)
- they aren't automatically Hashable and Equatable
- they aren't automatically included in the Enum's array of ".allCases"
The great thing about Swift is that all of these issues can be worked around using extensions. Unfortunately (the way I do things, anyways) that means a lot of the time in Swift, the Enum itself is maybe 5 lines long, while my extensions to work around the above limitations go on and on for another 30+ lines.
That sounds like a failure of a language to me. When you have to create long ugly hacks to work around the language's limitations, then it's not working for you but against you. I've been writing in Objective-C ever since abandoning Swift 2 and so far I've only really ever heard people say they're having this or that major difficulty with Swift, so it makes me feel like it was the right thing to do to avoid getting heavily invested in Swift.
Do you have any ideas for a more ergonomic way of accessing the fields? I'm asking because I'm working on an enum library for Python (shameless plug: [0]) and looking for ways to improve it, though of course it has different constraints.
To me at first glance this seems very similar to the rust trait system, where you start with a simple non object structure that describes only the functions that an object requires.
I know this is just an example, but it's distracting that a drawable is expressed as a set of paths. Order matters in drawing! (Generally, anyway) Actually, it's a bit bad that the collection is explicitly instantiated at all. It would be better if "paths" was only a sequence of Paths.
As someone who mostly develops in Go these days, I have envy of languages like Rust, Swift, OCaml and Haskell where "value-oriented programming" actually have rich language support.
In particular, "value types" are really useful for ASTs, configurations and other DSLs where you have graphs of polymorphic values.
For this, you really need sum types (often called tagged unions or enums), as illustrated in the article. In Go, the absence of a sum type mechanism means you have to use interfaces as a kludge. In a current project we have something like this:
type Expr interface {
isExpr()
}
type Identifier struct {
Name string
}
func (Identifier) isExpr()
type FuncCall struct {
Name string
Args []Expr
}
func (FuncCall) isExpr()
// ...
instead of something like:
type Expr enum (
Identifier struct {
Name string
}
FuncCall struct {
Name string
Args []*Expr
}
}
One annoyance in Go is that the compiler can't prove that your type switches are exhaustive, but there are other downsides, such as that all values must be boxed inside an interface.
As a Go programmer myself, I read this whole article thinking "well done, you've implemented Go's structs + interfaces model into Swift!"
I'm curious about the enum thing, though... I think the closest we get at the moment is the sql Null<Type> structs, where the value can either be null or a valid value, and we have to jump through hoops to work out which? Is that what you're talking about?
How would you see this working in Go? It is coming up to Go v2 after all, totally the right time to propose stuff like this...
The first part of the article (which summarizes a WWDC talk on protocols) presents something you can do with interfaces and structs in Go. But in the second part, where the author presents a superior pattern based on values and sum types, is precisely something you can't currently do in Go.
To do what he proposes, you need sum types akin to what Rust calls enums. In Rust it would be something like:
The idea is that you have a value declared as Path, you can only assign Arc and Line to it. Similarly, if you want use a value, you have to match against it:
match value {
Path::Arc => ...,
Path::Line => ...,
}
or you can destructure:
match value {
...,
Path::Line { start, end } => {
draw_line(start, end);
},
}
Languages that have sum types are usually expression-based, so the compiler insists that your match is exhaustive, or it won't compile:
// Fails with compilation error
match value {
Path::Arc => ...,
}
// This works
match value {
Path::Arc => { println!("is an arc"); },
_ => { println("is something else"); },
}
The closest Go has, as I said in my comment, is the ability to unify multiple structs with a dummy interface, e.g.:
type Path interface {
isPath()
}
type Arc struct {
center, radius CGPoint
startAngle, endAngle CGFloat
}
func (Arc) isPath()
// etc.
Now you can do a type switch:
switch t := path.(type) {
case Arc:
...
case Line:
...
}
However, since Go's interfaces are implicit, it doesn't know that Arc and Line fulfill a possible union of types, and so the switch cannot fail if it's not exhaustive:
// This compiles
switch t := path.(type) {
case Arc:
...
}
I'd love to have sum types in Go; it feels like a fairly natural fit for the language since we already have high-level constructs like interfaces:
type Path enum (
Arc {
center, radius CGPoint
startAngle, endAngle CGFloat
}
Line {
start, end CGPoint
}
)
The syntax for matching could be exactly the same as today, even. It would be to cool to be able match with destructuring, but given that Go doesn't support any destructuring today, I'm OK with this being left out to keep the language small.
As an aside, it's important to note that in Rust, enums aren't just structs; they can be tuples, too, or constant values:
enum State {
Alive,
Dead,
}
Here, a value of type State can be either Alive or Dead:
let s1: State = Alive;
let s2: State = Dead;
And you can pattern match against it. The closest Go has to this is numeric constants with help from the iota keyword:
type State int
const (
Alive State = iota
Dead
)
But yet again, this is a hack that doesn't provide much type-system safety:
thanks :) I get this now. This would be useful if Go supported it.
I've used enums before (but just as constant values), and I'd love to have them in Go. Iota is nice, but as you say there's no constraint on the type. It seems like a good fit...
60 comments
[ 2.7 ms ] story [ 123 ms ] thread"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.
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.
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.
Plus the runtime is excellent for server side.
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.
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.
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.
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.
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.
(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...)
As I understand (which again, might be flawed, please correct me), it would be more better to say that a type parameter bounded by a trait is a type, and a type implementing that trait is a subtype of the parameter?
But trait objects have types, and such can be subtyped, correct? That is, I can use a `&Bar` where a `&TraitFoo` is expected if `impl TraitFoo for Bar`.
Question marks, because I am not 100% that I have it correct, either. Hoping someone will correct. :)
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/
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.
"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'
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".
- 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...)
Technically, no, they can't. What you do is write your protocol as usual, then extend the protocol with a default implementation.
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.
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?)
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
https://github.com/franzinc/clim2/blob/master/utils/protocol...
https://github.com/franzinc/clim2/blob/master/clim/stream-de...
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/.
It is fun to see how the benefits of immutability, typeclasses, and (sort of) ADTs / pattern matching are being discovered independently.
Without this part, you end up with a real big mess similar to 90s internet.
Personally I’m fond of it. It’s one of several ways to blur the line between “is a” and “has a” relationships. We used to go out of our way to distinguish the two (eg in UML) but it has fallen out of favor.
A coworker long ago convinced me that improvements to delegation would be coming soon to new languages and unfortunately this is one of the first times I’ve seen it happen close to the way he hoped.
- they use this verbose syntax to access payloads "if case let .foo(bar) = baz { /* do something with bar */ }" The alternative being to do everything in a switch statement, and possibly create a local variable thanks to scope
- their payloads are not convertible to tuples, despite their cosmetic similarity
- they do not allow default values, despite their cosmetic similarity to swift function parameters (which do support default values)
- they aren't automatically Hashable and Equatable
- they aren't automatically included in the Enum's array of ".allCases"
The great thing about Swift is that all of these issues can be worked around using extensions. Unfortunately (the way I do things, anyways) that means a lot of the time in Swift, the Enum itself is maybe 5 lines long, while my extensions to work around the above limitations go on and on for another 30+ lines.
[0] https://github.com/lubieowoce/sumtype
I know this is just an example, but it's distracting that a drawable is expressed as a set of paths. Order matters in drawing! (Generally, anyway) Actually, it's a bit bad that the collection is explicitly instantiated at all. It would be better if "paths" was only a sequence of Paths.
In particular, "value types" are really useful for ASTs, configurations and other DSLs where you have graphs of polymorphic values.
For this, you really need sum types (often called tagged unions or enums), as illustrated in the article. In Go, the absence of a sum type mechanism means you have to use interfaces as a kludge. In a current project we have something like this:
instead of something like: One annoyance in Go is that the compiler can't prove that your type switches are exhaustive, but there are other downsides, such as that all values must be boxed inside an interface.I'm curious about the enum thing, though... I think the closest we get at the moment is the sql Null<Type> structs, where the value can either be null or a valid value, and we have to jump through hoops to work out which? Is that what you're talking about?
How would you see this working in Go? It is coming up to Go v2 after all, totally the right time to propose stuff like this...
To do what he proposes, you need sum types akin to what Rust calls enums. In Rust it would be something like:
The idea is that you have a value declared as Path, you can only assign Arc and Line to it. Similarly, if you want use a value, you have to match against it: or you can destructure: Languages that have sum types are usually expression-based, so the compiler insists that your match is exhaustive, or it won't compile: The closest Go has, as I said in my comment, is the ability to unify multiple structs with a dummy interface, e.g.: Now you can do a type switch: However, since Go's interfaces are implicit, it doesn't know that Arc and Line fulfill a possible union of types, and so the switch cannot fail if it's not exhaustive: I'd love to have sum types in Go; it feels like a fairly natural fit for the language since we already have high-level constructs like interfaces: The syntax for matching could be exactly the same as today, even. It would be to cool to be able match with destructuring, but given that Go doesn't support any destructuring today, I'm OK with this being left out to keep the language small.As an aside, it's important to note that in Rust, enums aren't just structs; they can be tuples, too, or constant values:
Here, a value of type State can be either Alive or Dead: And you can pattern match against it. The closest Go has to this is numeric constants with help from the iota keyword: But yet again, this is a hack that doesn't provide much type-system safety:I've used enums before (but just as constant values), and I'd love to have them in Go. Iota is nice, but as you say there's no constraint on the type. It seems like a good fit...