Programming thread

  • Want to keep track of this thread?
    Accounts can bookmark posts, watch threads for updates, and jump back to where you stopped reading.
    Create account
Fantastic post @Marvin . Lays bare the actual mechanics. Reminds me of seeing likewise in C. Too many OOP pros are functionally cargo cultists who couldn't write an OOP implementation, and it shows.
It's really a valuable exercise to engage in.

Once you've written it once, you can still use all the convenience macros but you'll never be confused about what's going on under the hood or why certain methods aren't getting called in the right order or whatever.

My implementation was attempting to exactly clone the logic of Chicken Scheme's COOPS, because that was my first exposure to it. Guile's GOOPS works similarly.

When I do my day-job work in Python or Go or whatever, I have a mental model that's my best "guess" about how their inheritance / method calling algorithms work and sometimes I'm fuzzy with it, but I can quickly whip up a test script to test my mental model. Like don't get me wrong, I've read the documentation or whatever, but sometimes I encounter weird inheritance behavior that I'm not expecting, but I know enough about how it could possibly work, that I'm able to make intelligent guesses and test them.

A lot of programmers nowadays have to drop everything and go to Stack Overflow to ask.
This is genuinely fascinating stuff. I haven't worked on a production Lisp code base but I have some intuitive guesses on how I'd define your macros like define-class and define-macro. And it's gotten me to thinking about what a mistake it is, that we don't weed out people who are too dumb to get recursion more quickly. *sigh*
The macros themselves are pretty simple, just wrappers around procedural functions that do the heavy lifting.

BTW they're all written in the low level Scheme macros. There was a push in the Scheme world to have completely "safe" macro systems that can't accidentally capture or accidentally shadow unwanted variables. And there's lots of super useful macros that can be written in them. But at the end of the day, sometimes you really do want to deliberately create new variables or shadow variables, so any good Scheme needs to still come with some form of low level macros.

Here's a comment from one of the macros:
Code:
     ;; (define-method (foo qualifier (arg1 typea) (arg2 typeb) arg3 . arg4)
     ;;   body ...)

     ;; =>

     ;; (define foo
     ;;   (update-wrapped-procedure!
     ;;    (ensure-wrapped-generic 'foo '(+ 3))
     ;;    'qualifier
     ;;    (make-method
     ;;     'arg-count '(+ 3)
     ;;     'arg-classes (list typea typeb)
     ;;     'func (lambda (arg1 arg2 arg3 . arg4) body ...)
     ;;    )
     ;;  )
     ;; )
One convenient thing from COOPS is that they don't require you to manually define the generic. They'll define an implicit generic if you define a method for a generic that doesn't exist. So the ensure-wrapped-generic call either grabs the existing one or inserts one in the generic table.

Then update-wrapped-procedure! call updates that generic to add the new method specialized on the classes in the define-method macro.

There's some other details, like there's a feature in COOPS where you can specialize on only the first X arguments and leave the rest as normal variables, which is why I have either a static number of typed arguments, or as above, three (plus some untyped variables) as (+ 3).

The qualifier refers to how COOPS (and other CLOS clones) permit before, after and around methods. So you can have code that runs before/after/around the main call. I haven't needed much use for that, but I guess can use it to set up and tear down state around the main call.

If that qualifier isn't provided there's just the default normal call qualifier.

Edit: Oh yeah, and totally re: recursion. If you can't handle recursion, you probably aren't really the best candidate for a career in programming. Even the most basic asm code can make use of recursive algorithms at times.

And in general, it's a mark of having the basic level of mental swiftness to be able to handle these concepts in general, to be a competent computer programmer.

Edit #2: Lol looking at my old repo, I first wrote this like 9 years ago.
 
CLOS (and CLOS knockoffs) are the best OOP languages I've ever used. And they're just implemented as macros in a Lisp.
I know only a little about CLOS; at least have heard of how amazing it is, and I just wanted to point out that Perl 5's third-party object system Moose built on top of the weird janky one in core Perl was heavily inspired by CLOS and Raku (formerly Perl 6) has a CLOS-inspired object system built right in. Julia (used for numerical computing and data science) is similar. It's not only for Lisp.
 
I know only a little about CLOS; at least have heard of how amazing it is, and I just wanted to point out that Perl 5's third-party object system Moose built on top of the weird janky one in core Perl was heavily inspired by CLOS and Raku (formerly Perl 6) has a CLOS-inspired object system built right in. Julia (used for numerical computing and data science) is similar. It's not only for Lisp.
Interesting.

Oh yeah, its principles can definitely be applied to other languages.

The main significance lisp has here is just that it can be implemented by fairly ordinary programmers and distributed as a separate library because lisp macros let you rewrite syntax in a far more accessible way than other languages, when they do have syntax extensions.
 
The main significance lisp has here is just that it can be implemented by fairly ordinary programmers and distributed as a separate library because lisp macros let you rewrite syntax in a far more accessible way than other languages, when they do have syntax extensions.
I do not yet "grok" macros but I have heard it said that the overall syntax of Lisp can pretty much be written down on the back of a two-liter soda bottle. On a related note, here's the famous Smalltalk postcard:
smalltalk-postcard.webp
 
I have heard it said that the overall syntax of Lisp can pretty much be written down on the back of a two-liter soda bottle
This really depends on the implementation form you want, and has all kinds of potential drawbacks, but there's even a Lisp that fits entirely in a 512-byte MBR bootsector now, if you can hold your nose at using trannyware. Tunney is one of the least ostentatious troons and just likes to write code. https://github.com/jart/sectorlisp/tree/main

yodawg.png
 
The main significance lisp has here is just that it can be implemented by fairly ordinary programmers and distributed as a separate library because lisp macros let you rewrite syntax in a far more accessible way than other languages, when they do have syntax extensions.
You seem to know way, way more about Lisp than I do, but this lines up with my understanding of it. I get how macros work but I haven't yet fully internalized everything that you can do with them, if that makes sense. There's a big jump from understanding it well enough that you can write some until or unless types of macros that are pretty brainless syntactic transformations to “fluency,” IMO. Macros and continuations are the things I really want to develop a good understanding of. Great stuff, dude.

Clicked this article thinking it would tell the reader to actually learn the language that underlies any given framework before the framework itself but then encountered the highly unpleasant surprise of a steaming pile of poojeetery:
I’m pretty sure dev.to is purely pajeet slop. It’s a shame that decent writing on computing has become so hard to find within the last, like, two years.
 
Also multiple inheritance is supported. I never understood why that was such a problem in other languages.
Namespacing, probably. In CL, slots are symbols; if you inherit from a class with a slot foo:x and a class from a different library with a slot bar:x, these are distinct symbols and won't cause issues. The same goes for generic functions. In many OOP languages, you just have one namespace for all fields/methods.
 
Multiple inheritances are a problem primarily because of diamond problem.
1768292707818.png
Basically once both of your parent classes share common parent it becomes ambiguous which class method should be called.
Or, probably more important, whether data for A should be duplicated so both B and C have their own copy, or maybe B and C should share singular parent.
 
Multiple inheritances are a problem primarily because of diamond problem.
View attachment 8413047
Basically once both of your parent classes share common parent it becomes ambiguous which class method should be called.
Or, probably more important, whether data for A should be duplicated so both B and C have their own copy, or maybe B and C should share singular parent.
Also, B and C could have implemented methods with the same signatures but which do something entirely different. (which you can run into with inheriting interfaces as well, but I digress (in c# you can specify which interfave the implementation is for so that it uses the right one when cast to that interface))
 
Multiple inheritances are a problem primarily because of diamond problem.
View attachment 8413047
Basically once both of your parent classes share common parent it becomes ambiguous which class method should be called.
Or, probably more important, whether data for A should be duplicated so both B and C have their own copy, or maybe B and C should share singular parent.
That's what I thought I remembered.

In CLOS, you can pretty easily determine the call order. It's just tracing up the inheritance chain, bottom to top, and then if any class inherits from multiple classes, left to right.

When inheriting class data members ("slots" in CLOS, as @306h4Ge5eJUJ mentioned) the list of slots in the lower inheriting class is just concatenating all the parents slots and then uniquifying that list.

I would probably actually have implemented it literally with (unique (append (class-slots bottom) (concatenate (map class-slots parents))) slot-equal?).
 
Last edited:
Basically once both of your parent classes share common parent it becomes ambiguous which class method should be called.
Or, probably more important, whether data for A should be duplicated so both B and C have their own copy, or maybe B and C should share singular parent.
It's also a problem when you consider vtables. Should the order be A, B, C? Should it be A, C, B? That entirely depends on how the inheritance was declared and neither would be compatible with the other.
 
I've always thought the issue with multiple inheritance to be "how do we deal with the fields of the common ancestor?" Unless methods don't affect fields of ancestors, the problem is always that either you have separate instances of the common ancestor (which begs the question of which one to consider as the "primary" ancestor of the instance at hand) or you have a common ancestral instance (which, in my opinion, is bad for consistency assuming that its descendants maintain some sort of invariant based on its fields). Are there any better solutions to this? CLOS seems to basically implement the second method, while Python implements the first, and C++ implements both (virtual and non-virtual bases).
 
Thought this would be worth mentioning:
Python.org has a page detailing how they implemented their multiple inheritance algorithm (C3 I think?) too, I'm procrastinating enough as it is though, so no direct link right now. You could even look at the implementation if you wanted to, IMO the CPython code base is pretty easy to read, but I'm also a crazy person.

I found out about the context type in Golang the other day, and it rubs me the wrong way. I can't articulate why, haven't done enough reading on it yet, but it feels like a code smell. Basically you can use it to manage operations that go across API boundaries or processes (like killing a web request that requires a database transaction, if for some reason you need to do that upstream), but most of the time you're passing it around, you don't actually use it? Maybe someone who writes large scale Go programs can explain why it's a logical design choice and I'm wrong, if I'm wrong.
 
I've always thought the issue with multiple inheritance to be "how do we deal with the fields of the common ancestor?" Unless methods don't affect fields of ancestors, the problem is always that either you have separate instances of the common ancestor (which begs the question of which one to consider as the "primary" ancestor of the instance at hand) or you have a common ancestral instance (which, in my opinion, is bad for consistency assuming that its descendants maintain some sort of invariant based on its fields). Are there any better solutions to this? CLOS seems to basically implement the second method, while Python implements the first, and C++ implements both (virtual and non-virtual bases).
Imho the second one is better, and should be the default. If you don't wanna share common ancestor state then you should be looking at composition over inheritance.
I personally never came across not wanting virtual bases in C++.
Though nowadays I rarely have more than single level of inheritance anyway.
 
Maybe someone who writes large scale Go programs can explain why it's a logical design choice and I'm wrong, if I'm wrong.
Don't overcomplicate it. It's a minor abstraction of a socket-like notion. The justification would come from that basis.

https://go.dev/blog/context - here is a needful article by Pajeet giving his explanation of its role. Seems to explain it well.

The notion is to staple on bits of requirement/etc. to a goroutine. I don't get what you dislike about it.
 
Back
Top Bottom