Hacker News new | ask | show | jobs
by ShaneWilton 3115 days ago
Lisp teaches you that there's always a better tool for the job than something that's already in your toolkit. More than any other language I've worked with, Lisp makes it incredibly easy and low-friction to write domain-specific languages to solve the exact problem you're working with.

That's not always a good thing -- it can make working with a foreign codebase difficult -- but it's definitely a powerful concept when applied correctly.

1 comments

Lisp seems to take the exact opposite approach as Go. The power of languages like Lisp appeal to me, so I have a hard time understanding why people want a language that intentionally limits itself. Read Graham's book and he's talking about how macros are great for writing maintainable code because you can make it both short and very readable because it's close to the domain.

But then you hear the arguments in favor of Go maintainability, and it's about copy/pasting being preferable to abstraction and not having too many ways to do things, so everyone's code looks familiar. Also that the extra LOC are more readable to Go maintainers than powerful abstractions. That surprises me, because languages like Lisp, Smalltalk, Ruby and Haskell are all about powerful abstraction capabilities so you can express yourself exactly as you need instead of writing a lot of boilerplate.

That surprises me, because languages like Lisp, Smalltalk, Ruby and Haskell are all about powerful abstraction capabilities so you can express yourself exactly as you need instead of writing a lot of boilerplate.

There is a mindset that's popular among programmers which says, "I cannot understand what anything does except by knowing all about its internals." Languages like Go let the programmer keep that mantra instead of understanding the things they use based on a description of external behavior.

The complex features, lots of syntax and many ways to do things are not a problem for experienced devs in a particular language. The real trouble comes when these experienced folks leave the project for something more challenging and you start with newbie devs with no experience and they feel demotivated when they come across these tricky features and abstractions which requires months of conditioning. At that point, all you want to do is make a meaningful contribution in the reasonable amount of time increasing your motivation level. The beginning is the hardest part.
That is the theory, the practice is the factory factory pattern, design pattern books, code generation frameworks, IDE plugins,error handling libraries,... all to workaround the language limitations.

So when a new one comes into the project there is this spaghetti of workarounds in place.

An old epi²gram:

There should be only one way to do it. — Python

There's more than one way to do it. — Perl

Do the right thing. — Lisp

"Do the right thing" is a brave statement for a language with no typechecking!
Plenty of typechecking primitives are available, and CL itself is strongly - but dynamically - typed. Now, the Lisp way strongly favours interactivity with programming, so by default, those typecheck helpers are not too convenient, to use, and a lot happens at runtime. But:

- CL standard defines a pretty decent type hierarchy (not Haskell-level decent, though).

- While not required by the standard, good CL implementations make use of typing for optimization and safety checks at compile-time. SBCL is particularly great in that domain, employing a solid type inference engine.

- CL allows you to override optimization/safety levels at very small granularity - even sub-function level - with (declare (optimize ...)) forms - like, you can e.g. drop (declare (optimize (speed 3) (safety 0))) inside a loop inside a function, to optimize just this particular section of code.

- Even though CL type-related primitives aren't very convenient (they generate a bit of line noise in the code), the macro system gives you all the power you need to hide it under whatever syntactic sugar you like. There's nothing stopping you from writing (or finding a library defining) e.g. a macro:

  (defun* foo ((real x) ((fixnum 0 100) y) z) -> rational
    ... some code ...)
that will get expanded into:

  (declaim (ftype (function (real (fixnum 0 100) t) rational) foo)
  (defun foo (x y z)
    (check-type x real)
    (check-type y (fixnum 0 100))
    (the rational (progn ... some code ...)))
which will give you both runtime checks and, with compilers at SBCL, plenty of compile-time type checks too.

--

I see "do the right thing" in the context of that epigram as meaning that you can choose to do the right thing without having to make compromises for syntax and semantics, as Lisp will happily let you remove any and all boilerplate with its macro system.

"Do the right thing", in this context, goes beyond just the presence or absence of typechecking.

But regarding type checking, Lisp (at least Common Lisp) is strongly typed. Really, very strongly typed (for example it will complain about putting a "byte" in a "character" array; or of using an "array" when a "simple-vector" was expected... Lisp is very nitpicky regarding types!), but the type checks happens mostly at runtime. Some checking also happens at compile time, even more if you intentionally include type declarations. (Type declarations are part of the ANSI Common Lisp standard.)

> Some checking also happens at compile time, even more if you intentionally include type declarations. (Type declarations are part of the ANSI Common Lisp standard.)

Just a note that this is entirely implementation-dependent. SBCL, for instance, is very good about using type declarations as correctness checks (those that can't be statically verified transparently degrade to runtime assertions) but their exact behaviour isn't specified in the standard; for example, implementations are free to take them as declarations that the programmer knows things the implementation doesn't and to trust them, which could cause weird bugs if they're not correct.

Emacs, one of the most reliable software ever was written in a language with no typechecking -- in Lisp! Emacs never crashes although it is configurable by the user in almost any way -- also in Lisp.
>Lisp seems to take the exact opposite approach as Go. The power of languages like Lisp appeal to me, so I have a hard time understanding why people want a language that intentionally limits itself.

Me too.

I don't understand how people in HN vouch for restrictive languages.

People don't want powerful features because they can be misapplied, making a mess. These people aren't worried that they will make a mess of their own code, they are worried that they will have to deal with someone else's mess.

What happens as you add more developers to a code base, with larger variance in ability and favored abstractions is going to be important in some cases, and irrelevant in others.

To expand a bit on this:

Imagine that you're joining a project. It's been worked on by a team of 100 people for a decade. Half of those people were below-average programmers. Many were newbies in the language, and some were newbies to programming. And you're going to get to try to maintain this code.

Now, do you want it to be written in a restrictive language, or in one that gives developers the ultimate amount of freedom?

Having worked both with big Java projects written by newbies, and on a large Common Lisp codebase that's older than I am (29), I don't really see much difference. Large projects are large, they always require time and effort to get into. That said, my current experience is that:

- A dumb language and simple code in a big project means lots and lots of code. On such codebase, my biggest issue is keeping track of how things fit together, because there's just so much of it (hint: they don't; people writing it can't keep track of all that stuff either).

- A powerful language and complex code in a big project means dense code. Like in that Lisp codebase, where I dealt with big macro-writing-macros, I would spend an hour with a macrostepper, trying to grok what a single line of code does. But once I did, that line of code (and similar lines in other places) were not a problem anymore, and they compressed what would otherwise be thousands of lines of boilerplate.

Which one I like more? I don't know. Big, old codebases suck, that's a fact of life. But I lean a bit towards "more Lispy" than "more Java-y", because it makes me feel I'm using my brain to actually think, instead of just tedious bookkeeping.

Surely in one that gives developers the ultimate amount of freedom.

I have seen enough C and Java code to see how creative developers and architects get to workaround the limitations of a restrictive language.

A convoluted code base full of design patterns, indirection layers, DSL and UI tooling for code generation, libraries to simulate language features.

>Many were newbies in the language, and some were newbies to programming.

I wouldn't join this project, no matter what the language is.

Imagine it's a popular languae. Javascript or C code, for example. There are no true namespacing / packages and modules facilities in JS or C. It would be even worse than the theoretical nightmare you think Lisp would be. (Lisp has extensive namespacing facilities; code can be contained within modules that don't clash.)

If it was Java, you will see wrongly applied Design Patterns, leading to over-complicated, hard to mantain code.

No, thanks. I wouldn't accept no matter the languages.

Newbies to programming should be educated and trained, not incorporated directly into an important project.

>>Many were newbies in the language, and some were newbies to programming. >I wouldn't join this project, no matter what the language is.

Then you will never be employed, because that describes virtually every software project at a for-profit company.

Also, less restrictive languages are computationally harder (most of the times so hard they become impossible in practice) to formally verify.