Live data from Hacker News

They Called It LISP for a Reason: List Processing (2005)

gigamonkeys.com

81–90 of 125 posts

Re: They Called It LISP for a Reason: List Processing (2005)

#81
post #51

The real elegance of Lisp is that malloc() has been renamed to (cons) and everyone feels smarter about it. I jest a little bit, but that's really the fundamental thing about list-processing. For most code, you don't really care about runtime, and you really just need "a data structure that probably can solve the problem", and the (cons) based list of car and cdr solves it. List processing itself is an elegant techniq…

I think building everything out of conses held Lisp back. It was possible, and it was deemed to be elegant, so people went too far with it. YMMV since my adventures with Lisp were around twenty years ago, but reading code, you'd see somebody constructing a list with a bunch of nested lists inside it, or some other complicated structure with conses, and you'd have no idea what it was or how they intended to use it, an…

In my experience, this is far less common in Clojure and far from idiomatic. The language’s core data structures and abstractions around them are all[1] based around `seq`, which is conceptually list-like in some respects. But among other distinctions (eg seq is presumed lazy by most APIs), it’s used to back various higher-level data structures, with various map structures in common use. There is an actual list type/structure which more or less works like cons, but it’s generally only used for code structure. I’m sure there’s a lot more that’s not “lisp” about Clojure, but I think this divergence is one of its big strengths.

From what I’ve seen, this also seems uncommon in Racket, but I haven’t actually written code in Racket or spent time in the ecosystem, so I can’t say nearly so much about what’s typical with much confidence.

1: Excepting reference types, which are all based around either transactional logic, “transients” which are scope-local mutables, or host VM interop.

Re: They Called It LISP for a Reason: List Processing (2005)

#82
post #51

Earlier quoted context omitted.

I think building everything out of conses held Lisp back. It was possible, and it was deemed to be elegant, so people went too far with it. YMMV since my adventures with Lisp were around twenty years ago, but reading code, you'd see somebody constructing a list with a bunch of nested lists inside it, or some other complicated structure with conses, and you'd have no idea what it was or how they intended to use it, an…

Wasn't this the Clojure approach? You don't need cons cells to have s-expressions and an elegant core set of operations and data structures.

Unless you have reason to worry about the internals and how they’re implemented (which last I used Clojure is unavoidable for non-trivial code, but I don’t think that’s intended), the data structures should all conceptually desugar to s-expressions. And they certainly do at the reader level. All of the non-list literal types are semantically equivalent to some kind of list representing how their values are defined or generated as a [specialization of] `(seq . …)`.

Re: They Called It LISP for a Reason: List Processing (2005)

#83
post #33
post #21

It might be as it started, however since 1970 that the various dialects support all common data structures.

I wonder what the minimal set of data structures is that you can build all others out of. For example binary trees can be built with lists, SEXPs are proof of that. You can of course also make a (boxed) linked list out of an array of 2 pointers, with one element pointing to the value and the other to the next element. You can make arrays with just pointer arithmetic, sooooo... just pointers is enough? (I guess this c…

Any sufficiently rich data structure is probably enough on its own. Lists certainly are. For example, you don't need arithmetic separately, since you can simulate it with lists in any number of ways; e.g., the von Neumann-esque `0 = ()` and `succ n = n : n`, according to which a number is represented by a canonical list of that length. Once you can define `succ`, you can define all other arithmetic. If you're willing to give up canonical representatives, and represent a number by any list of that length, then you get an easy definition of addition by concatenation.

If you'll forgive my giving an answer that clearly wasn't what you intended, rather famously, lambdas are sufficient: https://en.wikipedia.org/wiki/Lambda_calculus. See also https://en.wikipedia.org/wiki/Combinatory_logic.

Re: They Called It LISP for a Reason: List Processing (2005)

#84

Earlier quoted context omitted.

Macros just is code that transform a (list) data structure into (another different list), and then feeds (another different list) into the compiler (rather than the original (list) going into the compiler: how things happen without macros). You can see how these things happen in like, Chicken Scheme (R5RS Scheme to C compiler).

I want to look at it to understand. But my guess is that this happens at runtime, doesn't it. If this happens at runtime, this means runtime evaluates the macro, lisp function is generated, generated lisp function is compiled and used.

> If this happens at runtime, this means runtime evaluates the macro

Generally speaking this isn’t the case. There are probably exceptions besides a repl, but they’ll almost all be repl-like because macros are effectively functions of pre-compiled code, represented as lists as written in the code.

In other words, generally speaking, a lisp program’s life cycle goes something like this:

1. Developer writes lists.

2. Some of these lists are macros, compile them and execute them with their arguments. Many of these are themselves lists and symbols, which will be preserved according to various rules depending on which lisp you’re using and how they isolate code lists from program lists. It’s complicated.

3. Recurse step two until there are no more macro calls.

4. Now you have the actual program, and now you compile that.

5. Now you can execute the actual program.

Re: They Called It LISP for a Reason: List Processing (2005)

#85

>lists are an excellent data structure for representing any kind of heterogeneous and/or hierarchical data I found this a really curious statement. Linked lists made sense given the limitations in compute when LISP was invented (~1958) but how are vectors not a superior solution in every way, when available? Vectors give you fast random access and fast append plus the same first/next semantics and performance as list…

Lists based on cons cells avoid heap fragmentation: all allocations are of the same size. This is not true of vectors.

Making different lists that share the same tail, by building at the front, is incredibly common in code that manipulates code.

For instance, in many situations you have a body of forms that are to be put into some statement:

   (defun glue-args-and-body-making-lambda (args body)
      `(lambda ,args ,@body))
That lambda expression shares the args object with the caller, of course (and an array would do the same), but it also shares the body object, which an array won't do unless it's a very specialized array.

The backquote expression can compile down to:

   (list* 'lambda args body)
which allocates exactly two cons cells: one cell to hold lambda in its car; another one to hold args in its car and the body goes into the cdr of that same one.

It's also incredibly common to work with the suffix of a list.

In the reverse direction, we can break the lambda:

  (defun take-apart-lambda (lambda-expr)
    (destructuring-bind (lambda-sym args &rest body) lambda-expr
      (values args body)))
This requires no memory allocation We can pull the args out of an array-based lambda expression for free; but not the body (suffix of the array).

Vectors have the advantage of compact storage; a large vector's storage should be roughly the value word size times number of elements, plus small fixed overhead. Each element of a list costs us a cons cell (unless we have cdr coding, which is just little vectors in disguise). Cons cells can be compactly allocated, but not like the elements of a vector. They can be individually reclaimed though. If we cdr three steps down a list, and nothing has retained the pointer to that list, those three conses can be identified as garbage and reclaimed.

Re: They Called It LISP for a Reason: List Processing (2005)

#86
I'm seeing a lot of people sort of dismissing the usefulness of lists without really understanding why Lisp is important: aren't vectors and hash maps better? Isn't CONS just malloc()? Doesn't Forth solve the problem that low-level languages like C are inflexible?

— ⁂ —

The first thing to understand is that Lisp invented functional programming. (Seibel's chapter mentions this, of course.)

The functional-programming approach is to define your procedures recursively rather than iteratively; this began in Lisp but is now central to a number of other languages, like Haskell, OCaml, and F#. For certain fields, like symbolic algebra and compilers, this approach makes many difficult problems trivial. Even outside those fields, pervasive immutability often has many benefits, eliminating large classes of bugs, greatly simplifying problems like undo and thread-safety, and dramatically reducing the runtime cost of garbage collection. However, recursively defined linked data structures tend to use more space, and they're not very cache-friendly, and sometimes those are more important considerations.

To define your procedures recursively, it's very helpful to define your data types recursively. Recursively-defined lists (for example, "a list of Ts is either the empty list of Ts or a T followed by a list of Ts") support this recursive, immutable approach to programming.

Other kinds of recursive structures do, too; Haskell and ML dialects tend to use application-specific sum types at least as much as general-purpose lists, and I like that style better. It has most of the same advantages and disadvantages.

STL-style vectors do not support a recursive style of programming. They support an iterative, imperative approach to programming. If you use that other approach to programming exclusively, you will not understand why anyone would want to primarily use linked lists. And occasionally you will be confronted with problems that are very difficult for you to solve, problems which would have been trivial with a recursive approach, and you will not realize that you are doing a hundred times more work to solve them than you need to. SICP is full of examples.

— ⁂ —

The second thing about Lisp is that it has orthogonal serialization and deserialization: PRINT and READ. This mechanism is sometimes, though not always, adequate for debug logging, saving and loading application state, configuration files, and networking. Modern Lisps like Clojure generally extend those to arbitrary data structures, not just lists and atoms, but it's especially easy to implement if you only have lists and atoms; it's about 30 lines of Forth, for example¹. This is of course not unique to Lisp anymore; Java, Python, Tcl, Golang, and many other languages have ways to do it.

Orthogonal deserialization of lists is not really an optional extra, since you use it to parse Lisp programs, too. And you need the serialization to print lists in the REPL, anyway.

— ⁂ —

The third thing about Lisp is that it supports metaprogramming very well. This takes lots of forms; compile-time macros are a common one, and they give you enormous flexibility to extend Lisps into domain-specific languages, though they're probably used even more often to hack around inadequately optimized compilers. EVAL answers many requirements for runtime flexibility, though there are times when it is too powerful.

As with serialization and deserialization, these metaprogramming facilities mostly just fall out of the Lisp design; they require minimal or no extra code in a Lisp interpreter.

— ⁂ —

Historically speaking Lisp had a lot of other advantages: for a long time it was the only garbage-collected language, the only dynamically-typed language, the only language with EVAL, the only language with higher-order functions, and so on, and so for many years it was by far the best language for the things that you would do in JS, Python, Ruby, Lua, or OCaml today. JS, Python, Ruby, Lua, and OCaml are better for some of those things, and for many purposes they adequately support the functional, recursive, immutable approach to programming that Lisp pioneered.

Still, it might be easier to learn it in Lisp.

— ⁂ —

Lisp is sort of a minimal core of functional programming. In a decent low-level language like C or Forth, you can build a Lisp with an interactive functional programming environment with dynamic typing, orthogonal serialization and deserialization, an interactive interpreter, garbage collection, and extensive metaprogramming capabilities, in under 1000 lines of code, and the only data structure you need to do it is cons.

(It's not quite as simple as you might think from reading the "Maxwell's Equations of Software" in the Lisp 1.5 manual; those gloss over READ, PRINT, decimal conversion, arithmetic, symbol interning, the garbage collector, and user interaction, so you end up with significantly more code in a low-level language² where you have to implement those. But it's still days of work, not weeks.)

That doesn't mean Lisp is the only way to do functional programming, or even the best one. Quite apart from implementation questions, you might reasonably prefer the ML approach, with its strong static type checking; or the Haskell approach, which also features laziness; or the Clojure approach, which includes first-class persistent finite maps and integer-indexed "vectors"; or again the Clojure approach, where every operation supports ad-hoc polymorphism; or the Tcl approach, where everything is a string; or the Q approach, where your code defines term-rewriting rules rather than functions; or the Prolog approach, where you have not only list processing but lists that can contain uninstantiated logic variables; or the KANREN approach, which generalizes functional programming to full-on relational programming; or the Bicicleta approach, where you program directly in a side-effect-free ς-calculus, overriding methods instead of passing parameters; and many other approaches that haven't been thought of yet.

But if you're thinking of Forth as an alternative to Lisp, or STL vectors as an alternative to Lisp lists, and in general rather than in a particular case, you just haven't understood the Lisp approach to problem solving at all.

And when you do, it's going to be awesome.

______

¹ http://canonical.org/~kragen/sw/dev3/readprint.fs

² https://www.mail-archive.com/kragen-hacks@canonical.org/msg0...

Re: They Called It LISP for a Reason: List Processing (2005)

#89

Earlier quoted context omitted.

Forth solves this.

Forth is still interpreted. Forth's genius is that it's interpreter is so tiny that it fits as a runtime.

Forth can be interpreted but it can also be compiled in at least two or three different ways. Forth is unique in that its interpreter can look (and perform) a lot like compiled code.

Let's say you have a list of assembly language function names FOO, BAR, BAZ, etc. Each name stands for the starting memory address of that function. Each function ends with a standard RET instruction. At address PROGRAM, you store the list of 64-bit function addresses. Now a "Forth Interpreter" is just

  JSR-INDIRECT RP++
  REPEAT
where beforehand you've stashed PROGRAM in register RP. Most assemblers don't provide an indirect JSR instruction but it's easy to write a macro that serves that purpose.

Is this an interpreter? The only way it differs from true compiled code is that the function calls are indirect, so it's almost as fast as regular function calls.

What if you unroll the thing so the code looks like

  JSR FOO
  JSR BAR
  JSR BAZ
  ...
Now all the function calls are direct. And as a bonus, all the individual functions themselves don't have to do anything more special than directly call functions. It's turtles all the way down.

I've glossed over some issues like how to pass values and how many stacks you need and proactive tail-calling, but that's the gist of it. The whole "interpreter" just vanishes in a puff of smoke.

Re: They Called It LISP for a Reason: List Processing (2005)

#90

Earlier quoted context omitted.

Lisp can be ahead of time compiled, being dynamically typed doesn't impact that. It does impact what gets generated by the compiler. Compiled CL code is usually "generic", it has logic inside that helps it dispatch based on the dynamic types during the runtime but this is not interpretation (in the sense meant by TCL, Python, and others). You can also specify the types and a compiler can, optionally, make (or use) mo…

I know that lisp compilers compile compute heavy trivial functions directly to machine code. But how is the output of a program containing a lisp macro for example. Let's say define a lisp macro don't call it and generate its assembly. What is the machine code output? This is the part I'm speechless about. "The logic inside that helps dispatch based on the dynamic types at runtime" is the interpreter part IMHO. Plus…

> Plus you need logic to add the metaprogramming elements that require you to change the code after it has been written.

Not sure what you mean by this but perhaps you're referring to how Lisp allows redefining functions on the fly without relinking? That's done by indirect function calls. Every function call in Lisp jumps indirectly through the function's symbol name (if it has one). This incurs a runtime penalty of one extra memory access per function call, but it enables functions to be redefined on the fly without changing any of their callers. Optimization switches exist to get rid of this extra overhead if you need maximum speed in deployed code.

Post reply on HN