Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Languages you must know #10 (Lisp)
3 points by daly on April 10, 2021 | hide | past | favorite | 17 comments
Lisp

Lisp is a shapeless language. It enables you to shape your solution to your problem.

Lisp, unlike every other language I know, enable you to think about the problem and write the solution. Almost every other language has an "impedance problem", like connecting a soda straw to a firehose. Other languages require you to "forcefit your solution" to the language needs.

Lisp is THE language to learn. Thinking in Lisp is thinking about your problem.

Of all the languages I know, Lisp is the ultimate language.

Unlike other languages, Lisp is an "epiphany language". You don't "get it" until you "get it"... and then you wonder why other people don't "get it". From the dictionary we find:

An epiphany (from the ancient Greek ἐπιφάνεια, epiphanea, "manifestation, striking appearance") is an experience of a sudden and striking realization. ... Epiphanies are relatively rare occurrences and generally follow a process of significant thought about a problem.

Clearly the "process of significant thought" is about the problem. Lisp is the language of that breakthrough experience.

There are no words I can say, you have to have your own epiphany. (I could, I suppose, make some parenthetical remarks but that would be a bit meta).

Lisp is a "must know" language. It is, in fact, the most important "must know" language.



This is all false and misleading. Lisp is a language family, not any one language. There is no the Lisp, from which it follows that Lisp cannot be "THE language".

Are you saying that you can solve any problem with no impedance mismatch in AutoLisp?


As an architect I disliked Autocad and its scripting language, Autolisp which is the worse implementation of the Lisp way I know. It's when I discovered Scheme that I began to understand what the Lisp way could bring for me, as a non professional coder. And it's when I went down and analyzed its foundations, the lambda calculus, that I began to understand how a programming language can be built.

In the past, as a teacher in CAD, I have been writing rather useful code with Ruby working in Sketchup3D, and I loved this language. But today, as a past architect with time enough to think quietly (until death) about a programming language, the Lips way is my way: http://lambdaway.free.fr/lambdawalks/?view=fromroots2canopy

The only problem I encountered in my trip to the world lisp ... came from some Lisp priests who do not accept any other vision of Lisp than their own. And end up putting anyone off.

The only thing that matters is the pleasure one can take in elegantly coding beautiful algorithms. The rest is negligible, at least for me, who is nearing the end of my life.

Best regards

Alain Marty


False and misleading? The chosen languages have changed the way I've thought about programming. In my 'lived experience', Lisp is 'THE language' of thought. I'd welcome your choice of language and why you think it deeply affects the way you program.

I've used 'lisp' since 1970, in a FORTRAN implementation of Lisp 1.5 out of Bell Labs. Clearly 'lisp' has evolved since that time. But all versions I've used, like MacLisp, Symbolics, Interlisp, Lisp/370, VMLISP, CCL/MCL, Portable Standard Lisp, KCL, AKCL (I'm a minor author), Emacs Lisp, CLISP, Lucid, Gold Hill, ABCL, Clojure, and a few others I've probably forgotten. They are all 'different' but all are recognizably 'lisp'.

I don't know AutoLisp but I suspect that it might have the 'clay-like' property of other 'lisps'.

The point of this series is to find languages that change the way you think.

Don't confuse an implementation with the concepts.


Is this non-implementation language of thought documented somewhere, so that people can learn it and think with it? And does this language have mutable cons cells, with lists terminated by nil, which is a self-evaluating symbol that also denotes false? Are there interned symbols, quote and all that?


Assuming you're not trying to be sarcastic...

As a "language of thought" it is often the case that I will write my solution in Lisp and then "hand translate" it to the target language. This has two effects.

First, assume I'm "translating" to Python, for example. The Lisp "Gedanken" keeps me from depending on strange local language behavior. For example, I won't have to catch missing dictionary entries. But if I happen to want a local language behavior I can just make a macro.

Second, the Lisp "Gedanken" allows the ability to "factor" solutions since Lisp tends to "encourage" functions. For example, I'm working on a design for a dependently-typed language that allows proofs of the resulting programs. Lisp allows me to create a domain-specific language easily. Gedanken experiments led me to factor the category, domain, and representation for the specification, implementation, and proof languages. I know from experience that whatever domain-specific language constructs I dream up, they can all be implemented as macros and compiled efficiently.

When "thinking in Python", you'll find yourself restricted by implementation issues or searching for some library and then shaping your problem to fit the library. When "thinking in Java", you'll find yourself trying to force your solution into "objects". When "thinking in C++", you'll find that (a) you have to choose WHICH C++ (there are so many) and (b) by the time you implement something C++ will have changed.

I can still run Lisp code I wrote in 1980.

Assuming you ARE being sarcastic... there's Greenspun Tenth Rule:

Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp

The fact that you've immediately gone to "interned symbols" means you aren't used to using your language for thinking. These things don't arise in Lisp Gedanken.

Cogito, ergo Lisp


The thing is, if your language of thought doesn't have interned symbols, or conses, is it really Lisp?

Does your language of thought at least have first/rest recursion on lists, and do you use those idioms?

If you naively transliterate that to Python, that's not very performant or idiomatic; you can't work with Python people that way.

Are you sure your language of thought is really Lisp? Actual programs in a sophisticated Lisp dialect can't easily be spun off into Python or Java, at least if they are effectively using that dialect. If you want to treat Python through Lisp glasses, you may be best off getting one of the Lisp-over-Python implementations and adapting to them.

Over the past several years, I have written a working program in an actual Lisp dialect to try an idea, which I translated to Python (system requirement: not introducing anything new to a container).

I had to backpedal from some nice idioms in that Lisp dialect, and adapt to the Pythonic way of doing things. Part of the reason for that was people: the script had to pass code reviews, as well as lint programs like pylint3 and flake8.

> When "thinking in C++", you'll find that (a) you have to choose WHICH C++ (there are so many) and (b) by the time you implement something C++ will have changed.

If you think in terms of C++98 through maybe C++2003, you're okay. Hipsters working with your code will moan and groan that there is no emplace, move semantics or lambda, but not that it doesn't work.


Rich Hickey "thinks in lisp". For example, he talks about his "maps" data structure (aka association lists). See https://www.youtube.com/watch?v=aSEQfqNYNAc

Since data and programs are the same thing in lisp you can fluidly shift from one to the other, like I did with "knowledge" vs "rules". They both have the same representation (defstructs) but they are also code and have execution semantics. Clojure allows you to do this with map objects.

The fact that you can use the whole language on everything at any time and that everything at any time is also "the language" is how you "think in lisp". Nobody thinks of Python programs as "data" or data as Python programs.

Watch anything Rich Hickey talks about. He really "gets it".


Language limits what you can think. For example, you clearly feel that an interned symbol is essential to "lisp". But if I think about Lisp as lambda calculus notation there is no notion of "interned", yet it is still "lisp".

When I transliterate to Python I am not, by definition, worried about performance. Python has horrible performance.

Ah, the "Pythonic way" of doing things! Like the "Steve Jobs" way of doing things, aka the one-finger interface. "Steve Jobs thinking" would bring us a "self-driving car" that we "drove" by tracing a finger along a moving map, clicking on stop signs to stop. We'd be singing about how marvelous it is... it "automatically" blinks the turn signal when we trace a finger along a left turn! Python is the "Steve Jobs" of languages and thought: so easy! so intuitive! so limited!

You ask "Are you sure your language of thought is really Lisp?". I'm also a mathematician. You're asking me if I think about math in terms of multiplication. I do, but not the way you expect. MY multiply can be non-commutative if I'm working in matrix algebra. It can be a map function in category theory. It can be limited to finite fields or expanded to octonions. I'm still thinking of "multiply"; I'm just not limited to numbers.

Similarly, when I'm thinking about a problem my "lisp thinking" isn't limited to symbols. I worked on a Human-Robot cooperative task to change a car tire. I needed a knowledge base...lisp (aka Scone). I needed rules... lisp (aka ops5). I needed human language... lisp (my own code). I needed it all to work together so that rules could be represented as knowledge and knowledge could be expressed as rules (all were defstructs). When thinking about the problem I could write "lisp pseudocode" and then simply execute it. If I needed "multiply" to be non-commutative in some section of code I knew I could make it work trivially. Indeed I didn't even have to think about it.

Thinking in Lisp has nothing to do with interned symbols, just as thinking in math has nothing to do with numbers.

Thinking in Python is like doing math on a calculator. Everything is "Pythonic", but just like your "calculator" in math, limited to numbers.

I think the reason people "don't get lisp" until "they get lisp!" (aka the epiphany event) is that they stop thinking in other languages and begin to think in lisp. A whole new world opens.


Hi kazinator, are you replying to "daly" or to "martyalain"? If it is to me and about lambdatalk, some answer can be found in the links given previously.


I agree with you, Lisp is "the" language to meet. But I think that one can go a little beyond ... I would say before, towards its foundations. Lisp is a child of the lambda-calculus, Lisp focuses on lists (cons, car, cdr, ...) but IMHO I think that there are other choices, different ways which can be followed with some interesting results.

I would like to know what you can think of this one, the lambdaway project (http://lambdaway.free.fr/lambdaspeech/), easy to install and use in any web browser.

Alain Marty


Hi Marty; remember we had that discussion a few years ago about scoping and I pointed out that you can implement lexical scoping in the substitution-based evaluation strategy, like lambdaway. Did you ever try that?


Hi Kazinator,

Thank you for reply, I didn't remember our discussion and no, sadly, I didn't found how to implement lexical scoping until now. The workaround is a kind of "manual closure" as illustrated in

http://lambdaway.free.fr/lambdawalks/?view=closure http://lambdaway.free.fr/lambdawalks/?view=lambda

where is used the capability of lambdas to accept partial calls and IIFE build the bridge.

Time going on I improved lambdatalk, in a total loneliness. Except Ward Cunningham who finds some interest in a language which could replace in wikis the boring standard Markdown syntax.

I would like to know what you think of such an introduction http://lambdaway.free.fr/lambdawalks/?view=fromroots2canopy and, for instance http://lambdaway.free.fr/lambdawalks/?view=reduce

Thank you once more for your kind reply.

Alain Marty


I found some past discussions but not sure if that was the right one. Anyway, suppose we have a pencil-and-paper Lisp dialect, where everything is evaluated by rewriting objects on paper. We can have lexical scoping. That is to say, lexical scoping is achievable by substituting terms for variables. In particular, let us restrict to immutable variables. Like say we have

   (let ((x <whatever>) (y <whatever-else>))
     (body x y))
Suppose that is just a picture: a character string or pencil marks on paper. We see what the scope is of the let and can basically just substitute:

  (body <whatever> <whatever-else>)
It is the same with lambda. Say we have a lambda definition like

  (lambda (x y) (body x y))
When we call this with parameters <foo> and <bar>, whatever those things are, we substitute the arguments for x and y into the body: (body <foo> <bar>).

This substitution can take place by a recursive walk of the code which respects scope, so for instance, if we have:

  (lambda (x y) (let ((x <xyzzy>)) (body x y)))
the substitution will not just blindly replace the x as a character string. It walks the expression, and processes the (let ...) recursively, recognizing that x is shadowed by a new definition. The (let ...) part is recursively expanded and then substituted in place and so on.

You need to pass down some environment object during the expansion. But: your lambdas do not have to have captured environments! They can be fully substituted. So that is to say:

   (let ((x <captured>)) (lambda (y) (+ x y)))
can produce something like

   (lambda (y) (+ <captured> y))
no environment. The expansion/substitution process of (let ...) naturally took care of substituting the x value. The lambda parameter y remains unsubstituted: that happens at call time.

The substitute function should probably be aware of lambda and keep track of the y, but specially annotated in the environment as "this is a lambda variable: do not substitute now".

That sort of thing.

My main point is that if you don't need mutable environments, then pencil-and-paper lexical scoping can be simulated in character strings by substitution.

(Mutation throws a monkey wrench into things, like if you want to have stateful lambda procedures that return an incrementing counter whose initial value is a captured lexical.)


Thank you for these explanations.

As introduced in this page http://lambdaway.free.fr/lambdawalks/?view=lambda,

in lambdatalk everything begins with such a text replacement process:

  replace   :a0 :a1 ... an-1
       in   expression containing some occurences of :ai
       by   v0 v1 ... vp-1
rewritten in a prefixed parenthesized form

  {{lambda {:a0 :a1 ... an-1}
            expression containing some occurences of :ai  // it's a string
         } v0 v1 ... vp-1}
It's nothing but an IIFE, (Immediately Invoked Function Expression).

This is a simple example:

  1) {{lambda {x y} {+ x y}} 3 4} -> 7
  2) {{lambda {x y} {+ x y}} 3}   -> {lambda {y} {+ 3 y}}
  3) {{lambda {y} {+ 3 y}} 4}     -> 7
In lambdatalk the implementation makes lambdas true "combinators", knowing nothing but global constants (primitives and user defined ones). No free variables, no closures, pure functions, entirely independent of any context. Pure functions without any side effect. True referential transparency.

The lack of closures (I love them in Javascript) can fortunately be compensated by the fact that lambdas accept de facto "partial calls", and transfers of values can be done by "IIFE".

Arrays are the only one mutable objects and I use them to avoid recursion's stackoverflow, for instance in this page http://lambdaway.free.fr/lambdawalks/?view=reduce


So, I'm saying: that stuff could have closures, too.


I'am happy to know that that stuff can have closures but I'm not smart enough to implement them. I am a retired architect and not a professional coder. Meanwhile this is the workaround I use. For instance, the area of any triangle of sides[a,b,c] is given by the formula

  area = √s*(s-a)*(s-b)*(s-c) where s = (a+b+c)/2
If the intermediate variable s is provided alone in an IIFE, like this :

  {def area1 {lambda {:a :b :c}
    {{lambda {:s}                        // it's an IIFE where :s will be replaced ...
             {sqrt {* :s {- :s :a} {- :s :b} {- :s :c}}}    // ... here ...
     } {/ {+ :a :b :c} 2}}}}             // ... by this value computed once              
  -> area1 

  {area1 3 4 5}
  -> NaN                                 // Not a Number
it doesn't work, simply because [:a :b :c] are not in the inner function's arguments list and a function knows nothing about its context. It is necessary to manually add the arguments of the calling function

  {def area2 {lambda {:a :b :c}
    {{lambda {:a :b :c :s}        // :a :b :c are redefined ...
             {sqrt {* :s {- :s :a} {- :s :b} {- :s :c}}}
     } :a :b :c                   // ... get values from the outer func ...
       {/ {+ :a :b :c} 2}}        // ... and from the new computed value
  }} 
  -> area2  

  {area2 3 4 5}
  -> 6
For information lambdas are implemented at the beginning of this javascript file http://lambdaway.free.fr/lambdawalks/meca/JS.js I wouldn't want to complicate the code too much and especially lose the automatic partial application, which seems to me incompatible with closures.

Thank you for your kind remarks


>> kazinator

The let special form is a syntactic sugar for IIFEs which has the advantage of highlighting local variables ; so the previous area2 function can be rewritten this way:

  {def area3
   {lambda {:a :b :c}
    {let { {:a :a}                    // in fact, it's a hidden lambda
           {:b :b}                    // we must redefine the outer lambda's arguments
           {:c :c}                    // as kind of "manual" closure 
           {:s {/ {+ :a :b :c} 2}}    // and add the new computed variable
         } {sqrt {* :s {- :s :a} {- :s :b} {- :s :c}}}  everything is known
  }}}




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: