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
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().
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.