<https://programmingsimplicity.substack.com/p/mass...
# thinking-together
i
I want to respond to this point, since I think it's a good stand-in for many of your other criticisms of the schematic:
It is too complicated, it contains too much nuance and detail, the DI (Design Intent) is not obvious. Someone tried to improve the DI by colourizing the schematic, but, it still looks to be too complicated.
I'm not an EE, but I did spend almost 20 years working for a schematic "programming" company. To me, this schematic is about the typical complexity I'm used to seeing before you spill over to multiple pages. And the schematics I'm used to working with would come by the hundreds, bound in massive 3-ring binders, all describing a single piece of industrial equipment (like a single bulldozer or excavator). So going by gut, this schematic feels about as "complicated" as few hundred LoC, in that it'd probably take me about as long to do a cursory scan or deep read of either — less than a minute to survey the broad structure, a few minutes to identify patterns, and maybe 20 minutes to deeply go over everything. Is that "too complicated"? Too complicated… for what? Without presuming too much about your background (for instance, I wouldn't be the least bit surprised if you were an EE with lots of schematic-reading experience and still held this opinion), I think it's interesting to work through your "too complicated" reaction in a few ways. • How much of this detail and nuance is due to the design of schematics as a visual communication medium? Is there a different way of visualizing an electrical system that would be easier to read, while still being useful for the same purposes? Well, what's the purpose of a schematic? I believe they're primarily intended as documentation, useful if you need to repair an existing system to verify how it is supposed to work — they're a known-good reference you can compare your faulty system to, to identify short / open circuits, damaged components, etc. And, sort of like sheet music, these schematics elide plenty of information, such as the placement of components and routing of trace/wires — they aren't like blueprints. So maybe we could find a different way to do the same job, or a different way to do a different job, by choosing to elide different information. Or maybe we need to overhaul the "symbol language", so that instead of capturing information about what the components are and how they're connected, we capture information about the dynamics of the system. (Bret shows something like this in Inventing on Principle, but none of the EEs I showed it to found it at all compelling 🤷.) So — would your criticism be addressed if we changed the standard design of electrical schematics, and if so, how and why? • You rhetorically ask how many pieces of paper it'd take to hold the code for Pong in a modern language. I think that's a fascinating comparison. To make it fair, we'd probably have to find some way of normalizing the I/O APIs so that they're the same as what's available to schematic. After all, if there was a "runPong()" API that'd be unfair. Would the Canvas2d API be fair or unfair? Etc etc. My gut is that if you did this normalization, you'd probably end up with code that felt to a typical coder about as "complicated" as this schematic would feel to a typical EE. After all, they'd both be doing roughly the same thing, and I don't think Pong is inherently complex enough to benefit from the sort of abstraction-building that makes code so powerful. (That's my gut sense, at least.) But maybe you disagree, and the code would be fundamentally different in some way that'd change how complicated it appears. • The schematic uses space and visual symbols in a way that's fundamentally different from textual code. When you computerize the schematic, you can animate it, and reveal all sorts of information about the runtime dynamics of the system. At that point, I think the schematic is significantly more expressive than the equivalent textual code. So maybe, in the absence of that augmentation, the visual "juice" isn't worth the complexity "squeeze", so to speak. Anyway, thanks for the post. Hit the spot!
g
Yes, I learned to read schematics on my own, then I got an EE degree (which got me into compiler-writing! One 4th year course was "pick your own topic for the next two terms". Right at that moment, DDJ came out with its SmallC compiler article and my wire-wrapped Z80 was itching to be commanded (this was in an age of closed-source, so seeing other people's code was refreshing (the other thing I carried around in my briefcase was the source code to V7 UNIX)). I don't think that schematics are better than code, nor that code is better than schematics. I think that they both suffer from containing way too much detail splayed out in a meat-tenderizer method (bigger means wider, not taller (layered)). Both are difficult to understand unless you get a degree or misspend your youth learning their secrets. I do note: • when I built and debugged electronic circuits, I would be confident that they could be shipped with 0 bugs • when I build code, I always expect more hidden bugs to surface • I perceive our current software workflow as supporting the shipping of buggy designs (CI/CD, convincing unwary customers that they should act as our Q/A departments for free instead of suing us for selling them buggy products, quarterly Continuous Delivery updates, etc.) • I observe a major difference in the two techniques: code is sequential and synchronous, electronics is asynchronous and highly parallel even at low levels (cheapo ICs like the 7400 series) • I observe that both techniques are "too complicated" for getting a design overview • both techniques suffer from strongly-connected scoping - wires/globals poke through sides of sections and directly tweak the innards of sections. • debugging async systems was easier with hardware tools, like 'scopes, meters, than is debugging code with single-stepping debuggers and the plethora of doo-dads that have been pastied onto the single-stepping techniques to make them multi-thread-stepping. Sooo - what are "the good parts" of each? Can we pick and choose? I think that my 0D (PBP - Parts Based Programming) stuff is a step in that direction (surprise!). A system should be composed of multiple Parts, Parts must have well-defined Ports and must not allow their innards to be visible, tweaked, called by other Parts, i.e. data flow only (whereas FP transfers dataFlow+controlFlow). A system should be understandable in small (7+-5) chunks, digging deeper only by those interested in more detail (kinda like the colourized Pong schematic with all of the schematic details erased at the top level). It should be easily possible spray a design over multiple CPUs (functional thinking discourages that kind of thing due to its very low level over-synchronization / clockworkiness). Imagine each coloured box on the Pong diagram sitting on a separate Arduino with only thin wires connecting them through well-defined ports (my Wang word processor had a "bus" with 400 point-to-point wires on it to interconnect circuits plugged into the backplane - the S100 bus was much more general, hence, better and a breath of fresh air). [I have a "The Good Parts" article sitting on my disk, unpublished thus far. A lot of our programming techniques are based on 1960s biases, like concern for "efficiency". We should know better by now]
a
@Ivan Reese your comment makes me think of “alternative representations” (is there an official word for this?) of the same underlying thing. Like in Step Functions/Airflow, you can toggle back and forth between a code-representation of a DAG and a visual representation of a DAG. And with React/Vue developer tools, you can pull up a tree based view of your application alongside your application. I’m trying to think of more examples like this
o
I'm fascinated by the asynchronous nature of the schematic. Is this not (yet?) a van Neuman computer? It also doesn't compare to modern GPU's where each thread usually runs the same program on different data.. this is different programs (on the same data?) Maybe more comparable to something like micro services.. or threads in an operating system. I always felt like our programming model is too ego-centric. We think of the computer as an entity that does stuff (just like ourself). That's why we like textual programs, it is a recipe, from top to bottom.
g
I was inspired by the comments to respond and add some more thoughts to this thread, but the result was bigger than only a few lines... [Thoughts on Hindrances to Asynchronousity]( https://open.substack.com/pub/programmingsimplicity/p/thoughts-on-hindrances-to-asynchronousity?r=1egdky&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true)
m
Both text and schematics are "just" an encoding. This means you can come up with good/bad textual encoding describing PCBs, and with good/bad schematic describing bubble sort to be transpiled to e.g. python. So maybe the problem is "popular/familiar schematics/PL-syntaxes are not
very good for anything
, but `ok for everything`"? (cough https://futureofcoding.slack.com/archives/C5U3SEW6A/p1747153874428279 cough)
g
I read this to say that it's OK to settle for a mediocre PL. I strongly believe that language affects thought, and therefore affects how you solve problems and how easily you can solve problems. I know of at least two very common DSLs that are used in programming: BNF and REGEX. In physics, Richard Feynman used Feynman Diagrams to think through a hoary problem in physics. Syntax is cheap, paradigms are important. I think that it is much easier to create syntaxes today than it was in 1960. There is no good reason to settle on one mediocre syntax designed by someone else. Every project should have multiple syntaxes (and, not just textual). The remaining problem is: how to bolt many SCNs (nano-DSLs, Solution Centric Notations) together to form a whole solution. UNIX gave us some baby steps, but, we continue to believe that it's still 1960, that language design is hard and that only one language should be used per project.
k
Recommended reading: Notation as a tool of thought, Ken Iverson's Turing Award speech from 1979. The title says it all.
m
LISPs provide fairly easy (in the

simple made easy

sense: familiar, already at hand, convenient) way to make and use DLS. But maybe just 1% of all developers know LISP even exists. That, and memes like "using regex? now you have 2 problems", lisp curse, big ball of mud and worse is better suggest (ability to have) more syntaxes is not the only factor at play. I 100% agree that "good, fitting" DSL is a major writability, readability, maintainability factor. Need to solve integration UX and ergonomics, I guess. I myself try to replace clojure's imo bad DSLs with my own (destructuring, - threading>, control-flow, restructuring, etc.), but immediately run into missing IDE support for nontrivial cases (again, nod to tree-orientation vs line-orientation thread) so I need to re-implement macro multiple times for different contexts: actual runtime, then for IDE to understand and give me at least symbol-resolution and go-to-local-definition), and it is so painful, that writing my own IDE just to support these out the box is now not out of question (and is in fact in progress).
re "look at this hairy schematic": Essentially, this is the same spectrum as: from "single 100-lines-long function" to "100 clean-code-oneliners in 10 different files + 50 lines of composing those one liners", and it depends on the author and optimization goal: single function is way easier to debug, clean-code™ is easier to... I don't know, to (convince yourself you) get the gist maybe?
k
@Misha A The best IDE I know for supporting DSLs is DrRacket. If you follow the Racket Way of making DSLs, you get a lot of useful IDE support automatically.
m
@Konrad Hinsen at this point I am locked professionally to clojure, but I will dig into DSLs the Racket way to maybe borrow some ideas, thanks! If you have some specific docs or keywords to start from – please share (Module_Syntax?). (unfortunately, backward compatibility is not only about APIs, but about individual's habits, knowledge, experience, tools, and... a day job)
k
@Misha A A good entry point in the documentation is https://docs.racket-lang.org/guide/hash-languages.html. If you follow those instructions when defining a DSL, DrRacket supports it.
j
@Angus Mitchell the rough term for this is "protectional editing", at least when it comes to modifying the underlying representation.
i
"Protectional editing" is so damn good
g
Something that @Jack Rusher said in his talk made me think of this thread. Here is another attempt at explaining why I think that Lisp code is tree-oriented.
m
so, as soon as I implement read(str) in java, which would return a bunch of ArrayLists, Integers, Strings, Booleans and java.util.Maps – java code becomes tree-oriented? Is this the distinction?
g
The Lisp reader is different from read(str). The Lisp reader does extra work and parses the str and conses up a list. The lisp reader produces something like the diagram above, containing list (yellow) and atom (gray) cells. Other languages' read(str) doesn't do all of that extra work - in those cases read(str) simply returns a string.
m
I wrote "would return bunch of" (did not write "nested", but meant "nested"). You are talking about some "out the box" read() fns, which already "do not do" extra work, as if there is no way to write such fn at any time. But writing fn to convert string to a bunch of nested data structures is easy. + all the code-manipulations in the talk were done in the editor which is a separate system from lisp, which makes lisp having such read function out the box - irrelevant, because replace emacs with a text input on a website with a js fn which knows how to turn str into nested structure and you get tree-orientation too, right? regardless of which PL str it was: python, js, common lisp, java, etc. If that is all it takes to be tree-oriented... how is lisp special in this regard?
j
Programming language ASTs are tree shaped in general, which is not by itself an interesting observation. Lisp syntax happens to be a good representation of those trees. Also,
(read)
is great, but things get much better when you also have
(print foo)
.
g
These are very good points. I watch Rusher's demo and I know from personal experience that much (~10x) better productivity can be had. You point out that it is possible to break the technical aspects down into tiny pieces using just about any language. I argue that it is "not enough" to work /only/ on pieces that can be easily identified. It's like trying to explain to a frequency-only oriented technical person why vinyl sounds better than CDs (and MP3s, digital, etc.). It is my opinion that developer productivity tanked when the strong-typing / FP craze took over. Lisp, Forth, Smalltalk, Rebol, etc., etc. all have that "something" that got lost in the type-checking frenzy. As you point out, that "something" ain't a piece of tech. So, what is that "something"? Why do non-programmers like spreadsheets, yet pro programmers would never consider using spreadsheets for real programming? To say that emacs is not related to lisp is incorrect, in my experience. In fact, emacs is strongly related to pre-Common-Lisp lisp. For "some reason" (i.e. better productivity), emacs has led the way into creation and early adoption of a plethora of features, many of which have been Greenspuned into current editors and IDEs. Attempts to cut emacs over to Common Lisp have not borne fruit in terms of advancement of productivity and have mostly fallen to the wayside. Aside: note, too, that the concept of "macros" was invented in pre-Common-Lisp which used verboten technologies like FEXPRs. The concept of "macros" was later wrangled-over and tightened up in Scheme, but, the concept was not invented in Scheme. I said that the lisp reader is essentially a "scanner", but I failed to state and only implied that lisp doesn't bother to break compilation off and put it somewhere else in the workflow. Edicts like "don't use eval()" have harmed developer productivity (compilation == eval). Rusher points out that even Python's REPL ain't as good for productivity as lisp's, and, he proceeds to show why and comments on the relationship of /normalization/ of data representation and code representation. Poking at Rusher might be a good idea - we should extract from him more insights on what this means and how it helps productivity. Note that Rusher quotes "it's better to have 100 functions that operate on one data structure than 10 that operate on 10 data structures", but doesn't take it to the extreme conclusion that providing user-defined data structures ('structs', 'records') is a bad idea. Now that we're 50+ years down the road, I think that we should step back, take a deep breath, and observe what we've discovered about programming PtGP (Programming: the Good Parts). I've started to do this over on the "programming simplicity" discord and am looking for a better name (than the current "SCP") before proceeding much further. We need to recognize that just about everything they did in 1960 is old-fashioned and based on the erroneous presumption that computers are just mathematics machines that must be based on Gutenberg type-setting. In conclusion: lisp was tree-oriented from the start. The normalized data format - lists - used for data and for code and various seemingly-irrelevant design decisions (e.g. lisp scanner/parser/compiler built into the language). Today, we can duplicate this tree-orientedness using just about any programming language, but, we haven't managed to duplicate lisp's "productivity" for designing software (which is not the same as productivity for optimizing software). Rusher, Girba, Hinsen, Clojure+emacs, etc. are poking at design productivity instead of hard-core optimization. I see there being two different fields of programming - Engineering and Production Engineering (Design and Optimization). You can't schmoo both sets of goals into a single programming language, like we tried to do in 1960. Today, with better hardware and newly developed ideas about software, we can afford to build different tools aimed at different goals.
m
I am trying to pinpoint what tree-oriented actually is: Is it just a handful of functions, which happen to be in the std-lib of lisp, but not in other langs, but can be straightforwardly retrofit with a library to any lang. Or is it such combination of design and implementation decisions that only lisps have it, no matter how hard you try to put a lipstick on a python. "rename variable" and "extract as function" are tree-oriented features available for all languages in one IDE or another after all, so could it be that quacking like tree-orientation is enough to be tree-orientation? If so, then distinction would be not between languages but between IDEs/editors: • vim with this plugin for python is tree oriented, and this notepad.exe for lisp is not • default git diff tool is not tree oriented for anything, but difftastic is t-o for lisps • etc. re "would be nice to have print()": same argument: could it be retrofit onto js/etc? is it enough? Not every list is a valid code in lisp, not any tree is a valid clojure code, not every printed string would be valid js, but some would be, and there is an eval() in js... re "emacs+lisp": what I meant by "separate system" is emacs sends strings-of-code to repl (cider or nrepl etc), not pointer to the root of the tree emacs parsed code into, hence, conceptually, is not any different from html input with js.xhr.post(code-as-str) of string to repl socket. re "python shell is not a repl": is the only difference – python's shell is not your running project's process, but a separate one? so you can't overwrite some fn definition live.
j
In JS,
eval()
takes a string, so if you want to create and evaluate an expression at run time you must build a string representation. In any Lisp, it takes the AST to evaluate. The printer should be able to print everything the reader can read, and evaluation should be done on the result of read (which can also be built up with code without any string manipulation).
For those of us who have used lisps, the fact that the Python and JS interpreters contain fast, well-tested parsers for their syntaxes that we cannot use from within the language is a source of angry disappointment. Why should I need to write a JS parser in JS? They have a good one written in C++ already!
m
Python has at least https://docs.python.org/3/library/language.html. But it's true for JavaScript. There is no excuse.
g
I find this discussion to be quite stimulating. I summarize this discussion with a play-by-play: The graybeards say that programming could be more productive. The youngbloods are saying “but, we’ve identified and duplicated every piece of technology that you say leads to more productivity. Why is this not enough? What’s the missing piece(s)?. Maybe you’re simply tilting at windmills, maybe current programming is at least as productive as you think that old-fashioned programming is/was?” Jack Rusher’s video evidence shows that he could knock off some programming task using Clojure+emacs with the implication that this same kind of one-off productivity is not possible using other combinations of current PLs and IDEs, etc. The response is: there’s nothing here that hasn’t already been duplicated using current technology.
j
Yeah, we use this PLS stuff as part of a system that implements a probabilistic programming DSL in Python. It’s not a very elegant substitute for the things I’m talking about ☹️
@guitarvydas one of the things I think stands in the way of transmitting this message is that the magic lives in the gestalt, not in the individual features 🤷🏻‍♂️
m
Jack Rusher,
and evaluation should be done on the result of read
1. should it really? or "should to exactly match lisp api you are familiar with"? 2.
(-> "[:a]" read (conj :b) eval)
is not far from
(-> "[:a]" read (conj :b) str eval-from-str)
(this is what sending forms from emacs buffer to cider/nrepl/etc. repl actually does, so meh. Talking concepts here, not extra-compute/std-lib-vs-ext-lib-convenience, those ofc matter, but, imo, not for definition). To address your familiarity with api:
(defn eval-sexpr [sexpr] (-> sexpr str eval-string))
and you would never know :)
Why should I need to write a JS parser in JS?
this is comment on convenience, not on concept. Imagine tomorrow in Chrome/Firefox canary build you get evalJsFromString(s) objToCodeString(obj). Are we tree-oriented then? Again, only trying to zero-in on actual conditions of being worthy to be called tree-oriented.
programming could be more productive.
hell yes
Why is this not enough?
I think "there is no friction between concepts, but there is one between implementations"
j
Maybe think a little harder about how much work it is to build programs (or do anything else) through string concatenation vs through manipulating data structures. This is also the fatal flaw of SQL as a programmatic interface, btw.
w
Is it time to invoke Philip Greenspun's Tenth Rule of Programming? https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule
g
... that the magic lives in the gestalt ...
I continue to hope that we could further dissect the magic. Yes, there is something more than feature-set here, and I under-spoke when I used the phrase "tree-oriented". At present, I am thinking that it might be goals: (a) do you want to build an MVI (Minimum Viable Implementation, regardless of "efficiency" (not the same as MVP))? (b) do you want to use power tools to just get something done? (c) do you want to optimize a system for production release? I don't see all of these goals being satisfied by just one language, nor one class of languages (e.g. dataflow IPC vs. function-based IPC). For example, the fact that we can build AST data structures in many languages does not have the same effect as lispers getting ASTs under their fingernails. After 50+ years, can we graft some of the lisp-y magic onto our current workflows?
m
I see several things going for lisp in synergy: • homoiconicity (convenience, probably enabled macro system, and made it easier to provide print/read/eval out the box), • macro system (convenience, less error prone than C's string-templating, easier/faster/cheaper to implement DSLs) • out-the-box print/read/eval (convenience, interactive development, I bet many had idea for repl for JS, but implementing print/read was a deal breaker), • code compactness (in contrast to eg TypeScript/Java/etc explicit types: basically less text on screen, faster to keyboard-type it in, faster to try things out/iterate, and cheaper custom DSLs/control-flow speed this up even further) • homoiconicity also sets a baseline expectation on how custom DSLs (should) look like, so most popular ones are not over-the-top funky-looking but it seems like the underlying theme is convenience (with a touch of uniformity maybe)
g
The way I see it, the concept of macros came about due to features of pre-Common-Lisp and pre-Scheme lisps. Features such as FEXPRs and EVAL. Both of these ideas have essentially been erased from history. FEXPRs are like functions, except that calls to FEXPRs do not cause evaluation of args. FEXPRs are expected to evaluate their args when appropriate (using EVAL). These ideas have been formalized more tightly over time, but, the existence of loosey-goosey concepts like FEXPRs enabled the /invention/ of macros. Homoiconicity was part of the equation. Furthermore, the attempt to force all concepts into a single language, in a self-consistent manner, contributed to the erasure of FEXPRs. EVAL become "compilers" and was allowed to exist outside of formalized, consolidated programming languages. FEXPRs just went away. I feel that OhmJS/PEG bring back FEXPR-like features and I currently call the concept "transpilation". I expect it to run in a separate pass in kinda like a multi-headed UNIX pipeline. Issues of hygiene simply dissolve when separate passes are allowed.
k
Both FEXPR and macros are techniques to do staged computation. As are compilers, or more generally build systems. I'd love to see people focusing on the bigger picture of staged computation, rather than on the detailed differences of its various realizations.
👀 1
2
g
It's my impression that FEXPRs were condemned in the 1980s and essentially disappeared. Maybe that view is too narrow? Are there counter-examples that I should become aware of?
k
I am not much of an expert on the history of Lisp, but my impression is that FEXPR disappeared in the wake of the realization that eval was awesome but also dangerous. Your t2t is pretty much FEXPR, right? Across two languages rather than inside a single one, but that's a technical detail.
j
FEXPRs made static analysis, and this compiler optimization, too hard during the period of the 80s when getting Lisp to run fast enough was a major concern. There’s a paper about it by Kent Pitman. Edit: here’s an annotated copy of the paper https://www.nhplace.com/kent/Papers/Special-Forms.html
g
I think of of T2T primarily as "staged computation". T2T is macros (with syntax beyond parens). Is T2T FEXPRs? I don't know now. I just learned that FEXPRs can be treated as full-blown 1st class entities, like 1st class functions. Macros execute at "compile time". FEXPRs execute at "run time". Pondering ... @Konrad Hinsen
Eval() /is/ awesome. We use it every day under the politically correct name "compiler". The dangerous part is allowing end-users access to it or using it to slurp in used chewing gum that we peel off of dirty sidewalks. That, to me, doesn't mean that eval() should be expunged from programming languages. Production Engineers and their tools should detect instances of eval() leaking into production code, but, design Engineers should not be prohibited from using it. IMO, t2t does a lot of stuff - in a separate pass - that I would use eval() for.
💯 1
Poking at how FEXPRs were expunged, it appears that the first paper about this was Hart's 1963 proposal for macros. FEXPRs were invented by McCarthy in Lisp 1.5.