"Like the border between two warring states, the boundary between language and program is drawn and redrawn, until eventually it comes to rest along the mountains and rivers, the natural frontiers of your problem".
I wish pg programmed even today and kept writing about it.
It seems like what Paul is describing, is the niche filled by libraries/frameworks.
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]
> 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.
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".
Almost everyone should build their own framework on top of existing frameworks for their own domain. At work, we have a common look and feel for all our applications so that's build into the framework. We have specific domain specific data types -- those would never go into someone else's generic framework but they are in ours.
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.
I agree with you, the frameworks/libraries have done the bottom-up for you, to say, 70-80%. The other 20%, you need to work out on your own.
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.
I feel that the C ABI is local maximum, with a massive associated opportunity cost. Imagine if all the operators that have been written, "bottom up", in one language could be easily accessible from others. And the only way to leverage that today is to go through the crusty, unsafe API that no-one really wants to export to unless it's required in the spec of a project.
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.
Nothing stops a language runtime from creating a new ABI, and using it within the runtime.If it is much better than C and stable enough, people will adopt it. Compiler developers do not run away from that kind of project.
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.
I think PG's ultimate point was about metaprogramming, which kind of turns your request on its head. Don't bother writing python bindings to the operators you wrote bottom up in lisp or forth: metaprogram in lisp or forth to give yourself the parts of python you find useful for the project.
>, 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.)
I see what you mean that COM and Corba tried to unify programming across language boundaries. But they were not a low-level ABI type thing. They explicitly pushed a particular idea of what objects are and what they are for.
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.
Ive been recommending that, too. The better languages should use C's datatypes, calling conventions, have a seemless FFI, and generate C (at least optionally). This way, they can build on C's ecosystem instead of getting trampled by it. The C code can also be gradually rewritten.
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.
So how do you know that your low-level construct is on the path to where you want to be?
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:
The best way I know is to delay introducing the new abstraction until you have enough material to justify it. This means tolerating more repetition than you want the program to have in the long run, but only temporarily, while you build up a repertoire of cases that can eventually be abstracted away. You wait for the code to tell you how it 'wants' to become shorter, rather than trying to decide the primitives in advance. It's still just a bet that an abstraction will prove useful—i.e. will allow you to write future cases without thinking—and it still might not work out, but this puts the odds in your favor.
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.
There is an interesting parallel here between bottom-up programming and the common law system. Common-law courts don't create law in advance; they wait for conflicts to arise.
After enough similar conflicts arise and cases have been decided, an abstract "law" organically emerges that is used to decide future similar conflicts.
It's also kind of just like concept formation and theory building in science.
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."
That reminds me of a blog post[0] I once read by Casey. It does a great job of capturing my thoughts on the subject: in short, that premature abstraction is evil and a productivity killer. Many programmers fall into the trap because of how tempting it is to try and make everything neat and tidy, despite the costs in programmer productivity, program performance, and code maintainability.
I see the similarity but have a feeling it's a superficial one that leads to opposite conclusions. In the process I'm describing, the feedback loops remain short, there are many of them, and the overall state of the program never gets that far from "neat and tidy". In my experience, you can't program that way in C++ because the language and tooling impose too high a transaction cost on each feedback loop.
I find it's a valuable heuristic simply to try to make your code short (in tokens, of course, not characters).
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.
> I find it's a valuable heuristic simply to try to make your code short (in tokens, of course, not characters).
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.
I disagree. I recently read through Real World Haskell (http://book.realworldhaskell.org/read/ though it seems down right now?) as a refresher, and it devotes a lot of time to examples of the form "here is a three-line function, but aargh, it is too long, let's turn it into a two-line function by peppering it with <$> and <*> and <@!^!@> operators!". The savings in code size is small, but in every single case I found their "shorter", "simpler" program harder to read.
I suspect that's a pedagogical device to motivate introducing a style that can make code shorter and more readable, not a prescription to always convert three lines to two by using sigils.
But as a pedagogical device, it would seem to be teaching students to focus on the wrong thing (and it also fails Montrose's token-count definition of shorter.) (Caveat: the site is still down.)
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.
Reading some of the replies that interpret pg's essay as talking about "libraries" or "frameworks" or "metaprogramming" doesn't look quite right to me.
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.
> currency around 2008 so this old 1993 essay wasn't able to use it
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 time
metaprogramming 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.
>Embedding languages in Lisp is a concept since around 1962
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.
>What now is called 'embedded domain specific language' was called 'embedded language' in Lisp.
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".
That he could have used the term 'embedded language', which was used in Lisp decades before he wrote his article. So the article is probably not about embedded DSLs.
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.
>What he describes is actually extending the language to express the domain - but that does still not mean that the language is 'specific'.
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...
>", 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>.
> "extending the language" is described by some other Lisp programmers as creating a "DSL"
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.
>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.
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.
Whether DSL or not, Paul Graham does emphasize the use of macros, in more than one of of his essays. Not just the stock of macros that you get from an implementation, but the program's own. For instance, I read something of his some fifteen years ago where he claimed that it was typical for a Lisp program to be about 25% macros.
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".
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!")
The whole point of Forth was that you didn't write programs in Forth you wrote vocabularies in Forth. When you devised an application you wrote a hundred words or so that discussed the application and you used those hundred words to write a one line definition to solve the application. It is not easy to find those hundred words, but they exist, they always exist.
It is from the introduction to On Lisp, and there is a great deal of explicit source code in that. A lot of which is in fact about domain-specific languages.
You can still do something like User.create(data) using a bottom up approach in designing the interface. If we had a module User, we could do something like (using Haskell syntax, since that's the functional language I know):
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.
Interesting, I got the exact opposite experience with junior developers. They often try to design a system top-down. Fail to do so well enough because they don't have enough experience from the bottom up.
Interesting that your two examples would be called: object-oriented ("top-down") and functional ("bottom-up").
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..
Yeah, I second the preference for the latter approach. It encourages more reusable components - you get to use `format_user_data` in situations which don't require data validation, for example.
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
I believe the "bottom part" should be interchangeable, like switching to a different CPU architecture, or switching to a different framework. The bottom part is an implementation detail. The API is important since that's what you interact with.
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.
Beware, he was using the term "bottom up" in a meaning that diverges from current mainstream usage of the term: the bottom is the language, not the architecture.
I don't think bottom-up is a good way to describe the capabilities Lisp gives you.
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
On the practical front, this approach is tenable when programming for yourself or by yourself. How do you articulate this if you are consulting, your client isn't tuned to this thinking and want you to describe architectures and timelines? A statement like "I'll be working to create some primitives and based on what I see I'll choose to build or modify structures when appropriate." - doesn't fly. Though we might do that at the end of the day, I'm curious to hear here about what HNers think would be some good ways to talk about this process .. or not.
> Few would dispute, at least, that high level languages are more powerful than machine language.
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.
I've found a similar process by sketching up the domain logic, or typical steps of it, in as high level of pseudo-code as I can. Play around with it to try to optimize (factor) expressiveness. One then starts to see commonalities, and thus a domain-specific language(s) emerges. This language(s) may resemble or even become an API(s).
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.
65 comments
[ 3.4 ms ] story [ 117 ms ] threadI wish pg programmed even today and kept writing about it.
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 :)
[0] https://youtu.be/-I_jE0l7sYQ?t=21m55s
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.
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.
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.
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.
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.
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...
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.
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!
[0]: https://caseymuratori.com/blog_0015
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
- https://news.ycombinator.com/item?id=10872782
- http://archive.vector.org.uk/art10500700
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.
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 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.
[1] https://en.wikipedia.org/wiki/Domain-specific_language
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:
example for a LOOP form: > "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>.
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".
>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!")
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.
-- Chuck Moore, 1x Forth (1999)
http://paulgraham.com/onlisptext.html
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
in languages that support the pipeline operator.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.
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
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.
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.
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.