Programming Bottom-Up (1993)
paulgraham.com
paulgraham.com
I wish pg programmed even today and kept writing about it.
Quite often, you can see immediately that you need to represent X, that representing X requires foo() and bar(), and you can confidently make these things with a reasonable expectation of their usefulness. This already depends, however, on having done some sort of preliminary high-level design thinking. How much you need to do to be confident that you can identify an efficient set of primitives depends on what you are trying to do and how familiar you are with its problems. Writing primitives and utilities can simply be a way to avoid tackling the difficult bits.
For a cautionary tale providing a counterpoint to this article, there is this somewhat well-known case involving writing a Sudoku solver:
http://ravimohan.blogspot.in/2007/04/learning-from-sudoku-so...
That rule means that it's worth defining a new operator if you use it enough to save more than the amount of code you need to define it.
There are exceptions, of course. I have a higher standard for macros. But overall the principle of optimizing for brevity has held up through decades of programming of various types.
Make it short in characters as well.
Fitting more code on the screen makes it possible to see common subsequences and methods of abstraction not otherwise possible. It has dramatically improved my programming, and I recommend anyone who can to spend 6-8 months practicing it.
For more on the subject:
- https://news.ycombinator.com/item?id=13571159
More generally, I think there is an inversion of causality here: well-written code is often shorter than slapdash code with the same goal because the author thought about the problem as a whole, got straight to the point, and avoided adding complexity. I think that is clear in the Sudoku-solver example of my previous post.
It works the other way around too: when an abstraction such as a Lisp macro turns out not to be a good fit, simply inline all its usages and delete the macro. This makes the program more repetitive, but only temporarily, while you wait to spot a different abstraction that fits the material better. This again is listening to how the code 'wants' to be: if you get resistance (increased complexity) while moving in a certain direction, backtrack.
I try to give every concrete case veto power over a new abstraction. That is, if I've got six cases of a pattern in my code, and five can nicely be shortened with a macro but one is stubborn and refuses, I tend not to write that macro. I used to, but have observed that such abstractions tend to need undoing later. Instead, I'll hold off, tolerate temporary bloat, and wait for more cases. Perhaps when there are nine or ten, I'll get a new idea for a language construct that can express all of them nicely, and then I'll add that construct in the way that pg describes. It's like the Tao Te Ching says: "If you want to shrink something, you must first allow it to expand" [1]. When you hit on a good way to compress code, it feels like a satisfying click [2].
With such an approach, the program seems to breathe: expanding a little and contracting a little, but never straying too far from simple overall. Lisps are the best medium I know of for this. You get finer and more immediate feedback from the material of the code: less is consumed by the noise of the language itself, so it can be adapted to fit the problem better. Your brain fills up more with the dance between problem and solution instead of the externalities of the tools. And you have a wider range of tools for actually working the adaptation. The result feels more like molding clay with your hands than carving bricks with a chisel.
1. http://taotechingme.com/chapter-36-if-you-want-to-shrink-som...
2. https://www.npr.org/2010/12/30/132488837/The-Behind-The-Scen...
After enough similar conflicts arise and cases have been decided, an abstract "law" organically emerges that is used to decide future similar conflicts.
In algebraically or mathematically oriented communities like much of the Haskell world, you see a similar kind of "bottom-up programming" but with an added focus on compositional structures with laws for reasoning.
When a Haskell module really comes together beautifully, it's because you've managed to extract useful standalone concepts that combine in an algebraic way, with binary operators, associativity, and so on.
Eric Evans in Domain-Driven Design, usually considered a book about object orientation, also comes to a conclusion that if most of a program's logic can be expressed in pure functions and closed operations on value objects, then you can attain a "supple design."
I suppose the Lisp community has concepts like "value abstractions and syntactic abstractions" or something like that, one of them being related to well-suited data types, and the other being related to macros.
Incidentally, I've recently come to realize that Agda, the dependently-typed Haskell-style language, comes very close to being Lisp-like in its support for syntactic abstractions: you can go a long way with just custom mixfix operators (e.g., defining if_then_else_)... and then there's also a sophisticated macro system, using an AST data type and a "typechecker monad."
It's a lot of fun!
It's kind of growing the grass and let people walk on it, then decide where to put slabs on the paths.
Anyway I bet you are considering the cost of making changes. That cost is different for different languages.
When you have a website to build, you reach for an existing web framework, because it has already done the bottom-up steps for you.
The same could be said of choosing a language, OS, hardware, etc.
Existing technologies allow for increased productivity, at the cost of it becoming increasingly difficult to understand the bigger picture. The 'bottom-up' part at this point, is choosing which Ruby/Java/Python/etc libraries to use. The productivity bar has been set high, leaving little time for bottom-up decision making or education. Hence MongoDB and the never-ending tales of mediocre solutions gaining adoption due to marketing hype.
Curiously enough - academia isn't able to solve this problem well, because they're encouraged to produce 'novelty', rather than sit down and digest the mess we're in, to help clarify matters a little. They're in the same rat-race, of getting things done on a 6-12 month schedule.
There's a quote that I really like - "Prolific programmers contribute to certain disaster." [0]
Ah well :)
As I see it, it is from bottom to a greatest common denominator though. The tools you choose are not likely to have everything up to your "up", you still have to build your domain specific (but still abstract) "primitives".
I find this idea of building your own framework to be pretty rare -- I do it because I've actually had to build frameworks for platforms that didn't have them -- but most programmers are content to use what they're given but never think of extending it.
The final 20-30% are really marrying the exact business needs, with the underlying libraries/frameworks you're using.
The smaller the % of work needed to marry the business needs with the libraries, the more it starts to look like an application, like Photoshop, and less like programming.
How to live efficiently on the mess we are in is a perfectly good research area, one of the kind that severely improves the career of the researchers.
I argue that instead of those 3, the most applicable terminology to map to pg's idea would be a "DSL" ("Domain Specific Language".)[1] The "DSL" terminology seemed to have gained currency around 2008 so this old 1993 essay wasn't able to use it. In any case, his point is to program Lisp in such a way as to build a "DSL" inside of Lisp and you then program your higher level concepts on that DSL. His key quotes:
>a principle which could be called bottom-up design-- changing the language to suit the problem. In Lisp, you don't just write your program down toward the language, you also build the language up toward your program.
>Instead of a single, monolithic program, you will get a larger language with more abstract operators, and a smaller program written in it.
One can also pursue the "DSL" concept with other programming languages. In C++, you can design the classes to be a "DSL". In fact, one of the derided features of C++ such as operator-overloading was motivated by allowing a custom "DSL" to integrate into the language better.
Lisp is an interesting programming language for writing an embedded DSL because the custom "mini language" you write looks like other builtin Lisp syntax.
Why the other proposed terms don't seem to fit:
- "metaprogramming" : this is usually describing programs generating other programs either at compile time (e.g. templates) or runtime (e.g. the eval() function in Javascript/Python)
- "libraries" : these are often non-domain-specific reusable code... such as a string library, matrix math library, or a TCPIP library
- "framework" : this is usually a source-code runtime (basically an application "loop") with customization points to call your custom code
PG's essay like many other programming essays suffers from misinterpretation because it doesn't include any explicit source code to illustrate the ideas. Therefore, writing in metaphors invites misunderstanding.
-- Chuck Moore, 1x Forth (1999)
Embedding languages in Lisp is a concept since around 1962 (Daniel Bobrows METEOR Lisp extension for pattern based string processing). There are a bunch of Lisp articles written before 1980 which describe it and advocate using it.
> Lisp is an interesting programming language for writing an embedded DSL because the custom "mini language" you write looks like other builtin Lisp syntax.
Not really. Lisp is useful for DSL development because it provides the necessary extension features: which allows one to reuse a lot of the Lisp machinery (reader/parser, interpreter, compiler, macros, object-system, memory management, ...).
Common Lisp already comes with DSLs as part of the standard: format strings and LOOP are examples. They actually don't look like Lisp syntax - format strings are radically different and LOOP statements use more algol like syntax.
example for a FORMAT string:
CL-USER 2 > (format t
"~{~#[~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~}"
(list 1 2 3 4 5))
1, 2, 3, 4, and 5
NIL
example for a LOOP form: CL-USER 3 > (loop for i in '(1 324 2345 323 2 4 235 252)
when (oddp i)
do (print i)
and collect i into odd-numbers
and do (terpri)
else
collect i into even-numbers
finally
(return (values odd-numbers even-numbers)))
1
2345
323
235
(1 2345 323 235)
(324 2 4 252)
> "metaprogramming" : this is usually describing programs generating other programs either at compile timemetaprogramming means to be able to program on the language level. It has not much to do with program generation. For example in Lisp you can program new syntactic constructs via macros. One can also program the 'reader' which allows one to program the token reader. There are various other ways to program at the language level - the most complex is the Meta-Object Protocol of CLOS, the Common Lisp Object System - which allows one to program the object system system itself in itself.
metaprogramming facilities are tools to develop DSLs.
You misunderstood what I wrote. I was talking about the terminology PG used, not the concept. PG couldn't time-travel to the future and write "DSL" in his 1993 essay so more readers would instantly understand what he was talking about.
> it provides the necessary extension features: which allows one to reuse a lot of the Lisp machinery
I agree. The "looks built in" is a characterization that some Lisp programmers (maybe not you) would call the ability to add "new syntax" to Lisp. It's a way of contrasting with something like C++. You can't "extend the C++ language" with new syntax for your custom project unless you fork the GCC or Clang compiler and add new keywords to the parser. All new so-called capabilities to embed a "DSL" has to be done as "functions()" with obvious parentheses to call it. Lisp ("code is data!") doesn't have this hard boundary.
>metaprogramming means to be able to program on the language level.
I agree. There's parsing/manipulating its own AST as a data structure but "metaprogramming" also has lot of currency usage to describe programs writing programs and the usage of dynamic eval().
What now is called 'embedded domain specific language' was called 'embedded language' in Lisp. The name is slightly shorter, but the concept is/was the same.
Exactly! If you do "Ctrl+F" search on PG's essay, you'll see it does _not_ have the phrase "embedded language" nor "embedded domain specific language".
What exactly are you nitpicking?
What he describes is actually extending the language to express the domain - but that does still not mean that the language is 'specific'. It's not necessarily even an embedded language.
This appears to be a subjective labeling because "extending the language" is described by some other Lisp programmers as creating a "DSL". Some examples out in the wold:
>"Lisp is different, it is a shape shifting creature. It can be made to adapt to any environment [...], one can build a DSL (domain specific language) within Lisp to cater to that domain." From: https://www.quora.com/According-to-Paul-Graham-in-2002-langu...
>"Extending the syntax the way you're talking about allows you to create domain specific languages." From: https://softwareengineering.stackexchange.com/a/164674
>", we can extend it in Lisp itself [...] We've just created a very small and limited language for managing to-do lists embedded in Lisp. Such languages are very specific to a particular problem domain and are often referred to as domain specific languages or DSLs." From: http://www.defmacro.org/ramblings/lisp.html
What PG is describing may not be a complete and formal DSL according to your definition but others are describing his technique today as writing a <scarequote>DSL</scarequote>.
Modular domain specific languages and tools 1998 Domain specific embedded languages ACM servey 1996
Domain Specific Language has been as far back as the 70's. But, the term has slightly evolved over time.
PS: A lot of early computing history is really hard to find on the web now days.
Lisp can be extended in many ways without creating a DSL. That's the basic error you make. Paul Graham talks about extending/changing Lisp and you assume that this must be a DSL - while it is just a larger Lisp.
Typically bottom-up programming does not mean developing a DSL. Though it can be that one develops one or more DSLs during bottom-up programming. But it's not necessarily so.
Remember DSL means 'Domain specific language' - a language (that means syntax/semantics/pragmatics) specific (means offering a set of operations and data structures) to a domain (which has a problem which needs to be solved).
A larger Lisp is neither specific to a domain, nor a new language.
To give you an example: For some engineering problem (process control of chemical plant) one needs to express fuzzy values and needs to compute with them. PG advocates to implement these fuzzy data structures and operators. One then constructs further parts using these new building blocks. This could be a bunch of classes and functions. Just an extension of the existing Lisp - it would not only work with boolean values, but also with fuzzy values. While using the fuzzy operators, one extends them until all needed operators are available.
With the new set of operators large parts of the solution can be written in a shorter way. One can do such bottom-up programming in many languages and now also in many popular languages. One thing that made Lisp attractive early on for this style is the interactive development mode. One creates a new building block and one can immediately try it out - improving it and testing it again was always very fast. Thus one can extend the running program piece by piece and play around with the code in combination with domain data. Something which would not be typical in C, C++ and Java. Still, if one wants, one can do bottom-up programming in those languages too.
The result is neither specific to a domain (because all the other functionality is still there), nor would be a 'language' - it's just more vocabulary in an existing language - probably not even new syntax. One could also use the extension points of the language. For example Lisp would use T and NIL as constants for boolean values. One could extend the s-expression reader to add a new syntax for fuzzy values. That would not make a new specific language, but just extend the existing language along the prepared extension points.
You completely misunderstand my point. We're talking past each other. I absolutely do not assume PG's technique must be a DSL. I tried to be very careful in my wording in the very first comment to avoid claiming such a strong conclusion. Let me try again to clear up the confusion.
My perspective is about communication. A lot of people out there already have "DSL" in their head to describe what PG is doing. It's irrelevant whether it is right or wrong. It's the closest mapping people have. Others in this thread tried mapping it to "libraries" or "framework". I argue that "DSL" is much closer to what PG is describing to set off the "aha!" lightbulb moment in readers' minds.
Your perspective is about "DSL" being a formalized definition that PG's technique doesn't match and therefore it's not a DSL. I totally understand your point but it's not relevant to what I was explaining.
Yes, a "larger Lisp" may not be a DSL in your view but that's what many programmers are calling it when they extend Lisp to more closely match a domain. When PG writes "changing the language to suit the problem", many will naturally rewrite that in their head as "changing the language to suit the domain", and adding an operator is therefore "extending the language to more suit the domain". That's how people out in the wild are describing it even though there's no full-blown embedded Domain Specific Language there with formal semantics and grammar/syntax.
Tldr: you're taking "DSL" very literally instead of observing how programmers are using that phrase in the real world.
lispm is right that a bunch of macros don't necessarily make a domain-specific language. If I use PG's anaphoric if macro, or "with-gensyms", I'm not in a different domain; I just have some shorthand for what I'm already doing.
The "domain" doesn't have to necessarily mean different disciplines such as "game programming" is one domain and "online shopping" is another domain.
Thanks to montrose's reminder, I cite PG's "On Lisp" page 5 (which appears immediately after the "Programming Bottom-Up" section):
>We transform our software from a mere program into a _programming language_, and advanced users can build upon it the extra features that they need. [...] The simplest bottom-up programs consist of two layers: language and program. Complex programs may be written as a series of layers, each one acting as a programming language for the one above. If this philosophy is carried all the way up to the topmost layer, that layer becomes a programming language for the user.
Also in section "1.4 Extending Lisp" page 7:
>Lisp is an excellent language for writing compilers and interpreters, but it offers another way of defining a new language which is often more elegant and certainly much less work: to define the new language as a modification of Lisp. Then the parts of Lisp which can appear unchanged in the new language (e.g. arithmetic or I/O) can be used as is, and you only have to implement the parts which are different (e.g. control structure). A language implemented in this way is called an _embedded language_. Embedded languages are a natural outgrowth of bottom-up programming.
Yes, the 1993 time period predates today's "DSL" meme but now that "DSL" has permeated into widespread programmers' awareness, is there a better label to describe those PG paragraphs? I argue that today, people would rewrite PGs technique in their mind as "layering DSLs".
I too thought you were saying pg was suggesting a DSL
You argued the idea anyways, and “his point”
> I argue that instead of those 3, the most applicable terminology to map to pg's idea would be a "DSL" ("Domain Specific Language".)[1] The "DSL" terminology seemed to have gained currency around 2008 so this old 1993 essay wasn't able to use it. In any case, his point is to program Lisp in such a way as to build a "DSL" inside of Lisp and you then program your higher level concepts on that DSL.
I accept lispm's criticism and have acknowledged his idea of DSL is correct. I don't accept his mischaracterization of what I wrote and tried to clarify the confusion I caused. I accepted lispm's threshold for what constitutes a DSL. He did not seem to accept my DSL-as-a-meme.
>I too thought you were saying pg was suggesting a DSL
I acknowledge that you and lispm thought I claimed PG was recommending a _DSL_. What I tried to convey is that PG was talking about a concept that many programmers would loosely call a "DSL".
<formal>DSL</formal> != <scarequote>DSL</scarequote>.
<formal>DSL</formal> != <meme>DSL</meme>.
>You argued the idea anyways, and “his point”Yes... and I carefully wrote, "I argue that instead of those 3, the most applicable terminology to _map_ to pg's idea would be a "DSL":
I was hoping the word "map" was a sufficiently loose qualification to turn off the literal computer science definition of DSL. Instead of claiming PG's technique _is_ a DSL, I'm saying programmers out there have this informal usage of "DSL" (quotes) as a _meme_ and that is the closest terminology they would use. (See my 3 links showing examples of "DSL" being thrown around when Lisp syntax is extended.) Of the candidate phrases such as "library", "framework, "DSL", and PG metaphors such as "border between two warring states" -- the "DSL" meme is the closest _mapping_ even if PG himself might disagree and say that adding custom operators to better suit the program is not a _DSL_.
It's the same meme-vs-formal situation with C++. Some programmers would describe the design of classes as creating a "DSL"(quotes) within the C++ source code, but others would look at it and insist that they see no formal _DSL_ at all because there's no separate domain language that was explicitly defined and there's no separate text artifact that is parsed. (E.g. "Writing C++ classes isn't creating a DSL! That's just regular C++ programming!")
Swift soon being able to call python is a massive step forward, but a lower level API that wouldn't require a runtime yet still was superior to C's feels like a massive win for the industry that's just waiting to happen.
A lower-level binary API was tried before in the 1990s by various players. Microsoft's effort was COM/COM+ to unify programming languages across binary boundaries.[1] Another initiative was CORBA.[2]
Those technologies are still alive in various legacy forms but they haven't taken over the landscape like the proponents thought they would. (Although the whole MS .NET ecosystem introduced in 2002 is arguably a continuation of COM+ by a different name.)
[1] https://en.wikipedia.org/wiki/Component_Object_Model
[2] https://en.wikipedia.org/wiki/Common_Object_Request_Broker_A...
In any case, judged by the standard of becoming a universal ABI, they were also designed to "fail" in the sense that MS wasn't going too sit around and standardise it across operating systems, while the CORBA was a (feeble) response from the Unix world that no one expected would take off on Windows.
I'd say the problem is that nobody created anything that is much better and stable enough. That second one is a large problem on non-C languages.
On the LISP side, one might start with a combo of PreScheme with C compatibility, Rust-style memory management, and Racket-like DSL support. Maybe even embed it in DrRacket for tooling. Then, one can build up other Scheme-like features on top of it with an interpreted version.
So, that's an example of how to get the benefits of both.
I don't.
How would you feel if your client concerns themselves with the colour choices in your text editor?
Our job is to learn their business problem and deliver a solution. We are the expert at that, not them, so we choose how we want to do it.
You can go totally top-down in your design, which is the pattern I was encouraged to use by SICP ("wishful thinking" = top down). But Lisp doesn't make you stop when you reach the language level. "I wish my language would give me a concise way of saying whether variable x has the same value it had in the last iteration of this loop" is not a wish you can fulfill in a language without macros, for example.
It is the way I think. I am a very bottom-up thinker. If you give me the right kind of Tinker Toys, I can imagine the building. I can sit there and see primitives and recognize their power to build structures a half mile high, if only I had just one more to make it functionally complete. I can see those kinds of things. The converse is true, too, I think. I can’t— from the building—imagine the Tinker Toys. When I see a top-down description of a system or language that has infinite libraries described by layers and layers, all I just see is a morass. I can’t get a feel for it. I can’t understand how the pieces fit; I can’t understand something presented to me that’s very complex. Maybe I do what I do because if I built anything more complicated, I couldn’t understand it. I really must break it down into little pieces. - Ken Thompson, Unix and Beyond: An Interview with Ken Thompson, 1999
From a different direction, form a relational model, even if not using an RDBMS, to clarify the relationships between the domain parts.
Once you do both of these, then a bigger picture starts to form, and you refine it to have the API's work properly from the data relationship perspective.
I would! =) How is power of language defined?
Level of abstractions, high-level vs low-level? Attractive language syntax (DSL/macro/syntax sugar)? Tools, support and libraries? Community traction, popular, paid-more? Optimized and effective code? Low entry-barrier?
Depending on definition on what powerful language is, winners might become losers and vice versa.
Be a highlevel programmer: know enough languages to pick the right one for the task at hand. Do not take part in language wars.
a top-down approach would be clean and simple and the complexities are hidden away from interface:
User.create(data)
whereas, a bottom-up would be look like you just hacked things on top of each other:
create_user( validate_user_data(format_user_data(data) ) )
but, i do know lisp kinda forces you to code that way, so pg's article may have a lot to do with that.
create = User (validate . format $ data)
Only the create function needs be exposed, but the useful bit is that the create function is built out of the composition of smaller functions which makes it easy to change or completely swap out a component of the whole process. A top-down User.create function would likely be a rather large, monolithic chunk of code.
After a lifetime of programming, I actually prefer the latter approach: composing functions that are without side effects and independently testable.
I'm not sure if your examples reflect the top-down/bottom-up concepts in the original article, and certain don't agree that "a lot of jr developers" prefer the bottom-up approach, or that it looks "hacked on top of each other". Maybe it's just personal preference..
The one downside is the second style of code is a little uglier in languages without a good function composition operator, like Python. I enjoy writing stuff like
user = data
|> format_user_data
|> validate_user_data
in languages that support the pipeline operator.Need to sort a list? There's a sorting function in this library. Need to host a website? There's a framework over there for that.
The main takeaway seems to be the need to create utility level libraries for your own work that is specialized to your needs.
I think it's best to have as few dependencies as possible, try to keep everything independent and interchangeable. I like to view my code as a graph structure, try to keep the functions and its dependent functions as a Directed-Acyclic-Graph and avoid cycles.