Hacker News new | ask | show | jobs
by agumonkey 1877 days ago
Not to start a flamewar, every time I see a python talk (pycon or else), with fancy tricks like metaclasses.. all I can think is that, well, CLOS would have been perfectly fit for this too.

I know people are tired of the "lisp/smalltalk did it better" but what features of python are not possible (or hard) in CL[OS] ?

ps: how many CL shops are out there ? I'd work near free just to try a CL team once.

5 comments

CLOS (as you presumably know) models method application as calls to generic functions, instead of the now-more-mainstream Smalltalk-like message-dispatch approach which Python uses. The latter allows for things like overriding __getattr__ to intercept ‘all’ method calls and property accesses, for which I don't think there's any equivalent in CLOS.

The way methods are ‘attached’ to classes gives you a natural form of type-directed name lookup. CL generics have the advantage that you can define your own methods on existing classes while naming the methods in your own package so they don't conflict, but also have a curse of inconvenience along the way where importing a class doesn't naturally pull in everything associated with it, and you wind up writing the class name again and again when dealing with fields of an object. with-slots et al. are poor substitutes. (In an experimental sublanguage at one point I actually had local variables with object type declarations implicitly look up the class using the MOP and symbol-macrolet every available var.slot combination within the scope as a brute hack around the most common desirable case.)

Python's short infix/prefix operators are naturally generic, since they're implemented as method calls. In CL there's the generic-cl extension, but I haven't seen it have that much uptake… in particular, any library code that isn't explicitly aware of it won't use it ‘naturally’ on foreign objects, which could be good or bad.

That shades into the very-concrete type system that CL starts out with, where any attempt at ad-hoc polymorphic interop is a disaster unless everyone already agrees on what methods to use. I can't make a thing that acts like a hash table but uses a different implementation underneath, then pass it to something that expects to be able to gethash on it. I especially seem to get bitten by this in cases where alists are the expected way of representing key-value maps: there's no way to extricate yourself from the linear search without rewriting every piece of code that touches it, there's often an implicit contract that you don't want duplicate keys but it's easy to violate by accident and create bad behavior down the line, and so on. By comparison, Java collections in particular got this very right in terms of decoupling intention from implementation, and Python does basically the same thing but with a looser set of ‘expected’ methods.

By default, Python objects have a ‘purely’ dynamic set of properties, rather than the fixed slots CLOS imputes on an object via its class. Indeed the class-level property one can set in Python to constrain this for possible performance gains is called __slots__.

> The latter allows for things like overriding __getattr__ to intercept ‘all’ method calls and property accesses, for which I don't think there's any equivalent in CLOS.

CLOS already offered AOP, which you can use to control such calls.

https://lispcookbook.github.io/cl-cookbook/clos.html#dispatc...

I'm not sure what you're pointing at there. I'm aware of method combinations and method qualifiers—are you referring to being able to add a general :before/:after/:around on a single generic? If so, that's not what I mean; what I mean is vaguely similar but on the first-arg dispatch axis. Here's a toy Python example. Given:

  class KnowItAll(object):
      def __getattr__(self, attr):
          return lambda: "yes, I know how to " + attr
We then have:

  >>> k = KnowItAll()
  >>> k.reveal()
  'yes, I know how to reveal'
  >>> k.transfigure()
  'yes, I know how to transfigure'
  >>> k.make_sandwiches()
  'yes, I know how to make_sandwiches'
Ruby does this with method_missing instead, which is where I'm most used to it happening (and I think it's used a lot more in Ruby than in Python owing to the language-culture's higher tolerance for magic). Smalltalk used doesNotUnderstand, IIRC. One of the key secondary results of this is that you can do things like https://paste.ee/p/tUaRP, which is a toy “Tracer” class which intercepts, prints, and forwards method calls and attribute accesses (ignoring some edge cases).

If we were in CLOS, and started with:

  ;;; widget.lisp
  (defclass widget () ((radius :initarg :radius)))
  (defmethod grow ((w widget)) (incf (slot-value w 'radius)))
What I would expect for the equivalent is that, given:

  ;;; tracer.lisp
  (declaim (ftype (function (t) t) make-tracer))
  (defun make-tracer (object) ...)
  ;; ... further code goes here ...
Somewhere else, we can do:

  ;;; fiddle-with-widgets.lisp
  (let* ((w (make-instance 'widget :radius 3))
         (w* (make-tracer w)))
    ;; ???
    (grow w*))
Can you add code to tracer.lisp, without specific reference to anything from widget.lisp, such that this has the effect of (grow w) but prints what it's doing? Note also that my use of slot-value above is very deliberately a ‘raw’ access. I'm here completely ignoring the “make up entirely new methods on the fly as needed” part that method_missing also gets used for, which is even more impossible in CLOS given that it would require intercepting, what, all symbol lookups…

In CLOS, the class doesn't ‘own’ the method, it provides a type for dispatching on, so there's no way to do “give me some control over every generic function so long as the first arg is of ‘my’ type”. Which is a reasonable model, but means you can't do the same thing. Indeed the flip side is that in the Smalltalk-like model, generics are not reified, and methods that are specializations of the ‘same’ thing have no ‘real’ identity to them, so you can't do a type-ignoring :around method for an ‘entire generic’. (Often there will be a superclass to attach to instead, but it's considered dangerous “monkey patching” to mess around with someone else's class hierarchy like that, and in the case of more abstract interfaces there's nothing.)

Does that make sense?

Aha! I was half-wrong, but it's also horrible…

  (defclass tracer () ((actual :initarg :actual)))
  (defun make-tracer (object)
    (make-instance 'tracer :actual object))

  ;; But please don't.
  (defmethod no-applicable-method :around (gf &rest args)
    (if (typep (car args) 'tracer)
        (let* ((tracer (car args))
               (args* (cdr args))
               (actual (slot-value tracer 'actual)))
          (format t "Calling ~S on ~S" gf (cons actual args*))
          (apply gf actual args*))
        (apply #'call-next-method gf args)))
You can't do this with a real specialization on no-applicable-method, incidentally, because the first arg isn't special enough, it's just folded into the &rest. And that, I'm pretty sure, means this doesn't coexist with other uses of no-applicable-method properly… and you still can't do on-the-fly method names that aren't attached to a generic, and so on, but this does sort of account for the object forwarder case (and in fact you could extend it to allow tracers on more of the arguments!). It does, I expect, remain extremely unidiomatic by comparison.
thanks for your points, it's true that python dynamic operator genericity is very handy
> every time I see a python talk (pycon or else), with fancy tricks like metaclasses..

All I can think of is what a mistake those features were in python. "Fancy tricks" are generally the author trying to be clever (in the Kernighan sense) and ends up obfuscating the result. Not saying it works this way in CL, I don't have the experience to make the call, but in Python it was (and probably still is) prevalent.

The goal is for a little bit of clever work to make the computer do a lot more stupid work. People get bored and make mistakes, and our brains are not getting any faster, so this kind of leverage is the only game in town.
I am sorry but what does this mean:

> the author trying to be clever (in the Kernighan sense)

I know that Kernighan is the author of the C book, but its been a while since I skimmed it.

Please email hello@atlas.engineer with your details. We are a CL shop responsible for Nyxt browser. Thanks for your time!
Beginner approachability seems to be the key feature. Possibly also integration with external libraries.
This is my impression, too. When I was TAing an intro CS course back in university, I saw students new to programming struggling with Lisp (specifically Scheme in this case) much earlier in the learning process than even C.

This manifested in a few spots. The first is, yes, s-expressions. Yes, yes, I know, s-expressions are integral to the power of lisp, and they're really not hard to read once you get used to them. All of that is beside the point. The reality I saw on the ground, when teaching people to program, is that even people who have no prior programming experience whatsoever, and therefore no preconceptions to get over, have a harder time grasping s-expressions than they do algol-style syntax. I don't know why. I didn't have the same problem myself. But it's a real phenomenon that I struggled to help people through on a regular basis, and the lisp community's defensiveness about it is not going to make it go away.

Arbitrary-seeming names with zero mnemonic value is another problem. Car, cdr, progn, etc. - it takes a special kind of personality to not be put off by this sort of stuff. Not everyone has that kind of personality. Not everyone should have that kind of personality.

Finally, all the hair-splitty (at least to a newcomer) distinctions to contend with. =, eq, eql, equal, and equalp, or let, let* and letrec. Sure, there are reasons for these distinctions. But a language that can get by without quite so many of them is going to be a lot more attractive to newcomers. Even if it comes at the cost of footguns, if they're unlikey to be discovered until later.

That's my take on it, but I've ran into people praising it like it was alien theory of everything dropped by gods as a gift.

I like python but I'm a bit fed up with the mobthink (how surprising).

Another factor, I think, is that a heck of a lot of people don't think they will still be doing this for a long time.

Programming is something interesting and fun they are going to do for a few years while young, until (pick one) {their band takes off, someone funds their startup idea and they hire others to do the programming while they generate genius ideas and run the business, they get promoted to a high paying executive position that involved management and architecture and others do the coding, their podcast becomes a hit and they can live off that, the small company they work at IPOs or gets bought and they make enough to retire at 30, etc).

So they learn a fairly easy language that has lots of libraries that cover most things you do in a routine developer job.

...and before they know it they are 50-60 and still writing a lot of code, and realizing that if they had known they would still be doing this 30-40 years later they would have been better off if they had learned and used and gotten good with some of the languages that have a reputation of being very productive but hard to learn.

I'd also add spreadsheets and database to that. At one point I was the database guy at work, because no one else was available. I learned enough SQL to get by, but was in no way a database expert. Heck, we had to pick job titles at one point that described what we did to have on the business cards the company was giving us, and I put down "Database Roustabout" [1], which should give you an idea of where I stood. That was 20 years and I'm still the database guy at work. It would have been a lot better if sometime early on I had said "I'm going to become really good at SQL even though I'm sure someone else will become database guy in a year or so".

[1] Roustabout. NOUN. An unskilled or casual laborer. (North American) A circus laborer.

Yeah life as a weird tendency to not turn out like one anticipates :)
It might serve to evaluate why you feel the way you feel on a deeper level. Based only on your comments here I don't think any ire is justified.

For someone to to even make the determination that "Lisp did my better" they must

A. Have a comprehensive knowledge of both Python and Lisp.

B. Have some understanding of the history of the languages.

C. Understand the problem on an intrinsic, fundamental level that enables them to evaluate which approach is better.

D. Generalize the problem to a broader scope to demonstrate why one language is better on a broader level.

E. Understand every other language ever used so when they show why Lisp does it better, they can defend themselves when someone comes up and tells them, "actually, Pascal does this even better yet."

Furthermore, it doesn't really matter what language did it better. The point of most talks is simply to demonstrate how to solve some problem in a certain language. If every talk started with "you can do this in Python, and I'll show you how, but you should probably just switch to Lisp because it does it better," that isn't very helpful.

Oh I don't feel ire, maybe some form of frustration at best (i'm not young enough anymore to get angry for a talk :)

It's more about the 'wheel reinvention' syndrome that creates fatigue in me.