Learnable Programming

Bret Victor wrote another great essay, Learnable Programming: Designing a programming system for understanding programs, in the wake of StrangeLoop.

The goals of a programming system should be:

  • to support and encourage powerful ways of thinking
  • to enable programmers to see and understand the execution of their programs

A live-coding Processing environment addresses neither of these goals. JavaScript and Processing are poorly-designed languages that support weak ways of thinking, and ignore decades of learning about learning. And live coding, as a standalone feature, is worthless.

Alan Perlis wrote, "To understand a program, you must become both the machine and the program." This view is a mistake, and it is this widespread and virulent mistake that keeps programming a difficult and obscure art. A person is not a machine, and should not be forced to think like one.

How do we get people to understand programming?

We change programming. We turn it into something that's understandable by people.

Bret Victor writes in a flowing, highly accessible, and richly exampled style that I have, perhaps unfairly, come to expect from him. This essay will be of great interest to anyone who is exploring live programming, interactive programming, augmented programming, or integration of programming language with development environment.

TOPLAP, a community for live coding since 2004, provides a little extra context for the essay.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I use tools...

...somewhat like he describes everyday, as part of my daily work. I can vouch for their scalability (I work on large applications and web services); but more importantly, I can verify that making things more understandable is a definite productivity benefit even for a long-time programmer like me. Tools like IntelliSense, code completion, designers, interactive tracing and time-travelling debuggers aren't just "crutches", they make it possible to think about much larger pieces of code because they make the behaviour of said code more transparent.

Where this intersects programming languages, for me, is that some kinds of interactions seem more "natural" when applied to some kinds of languages; Bret points that out, indirectly, when he refactors a for() loop into a while() loop to make its operation more visible. That has me wondering if some of the techniques he uses to provide visual explanations of code (marble diagrams, inline contextual commentary) would benefit from a somewhat less imperative language.

I actually think you'd

I actually think you'd rather go the reverse way: take some dense for/loop comprehensions (e.g., written in LINQ) and transform them into more readable (and vertical) imperative statements for the sake of understanding. Higher order functions are powerful but not usable; they are concise but not accessible.

Imperative programming has an important advantage: it has a notion of implicit time and therefore you can "step" to the next statement, whereas functional languages are timeless and you have to think of another way of debugging your executing code. Unfortunately, I'm not aware of any good FP debuggers, that help you reason about the program's timeless dataflow in a specific context; at best we can debug the imperative interpreter. But I would be happy if someone could prove me wrong here.

Marble diagrams

Marble diagrams are really, really useful for reasoning through reactive systems (cf the plethora of such diagrams in discussions around Rx). I'd love to see something like LINQPad expose such diagrams on "live" Reactive expressions; I could swear that I recall someone working on such a project, but I can't dig it up just now.

The best FP debugger I ever used...

Was a lisp debugger that rendered the program (visually) as a tree, with the root node in a drawn box at the left and dependent subnodes joined to it by lines to the right. This is simple and works specifically well for Lisps because their abstract syntax tree is visually apparent from the source code, and the text structure is of course isomorphic to the tree structure.

When you "stepped" the program, nodes of the tree would get annotated with the values generated for those subexpressions during the run. You could pick-and-choose (mouse-click) nodes in any order to compute their values (of course their subnodes would get computed in the process).

I don't remember what it was called; it was on a LISP system I used at uni for a Honeywell mainframe way back in the wayback.

Awesome! Would really love

Awesome! Would really love to know what debugger this was called if anyone knows.

Timeless

Imperative programming has an important advantage: it has a notion of implicit time and therefore you can "step" to the next statement, whereas functional languages are timeless and you have to think of another way of debugging your executing code.

IIRC a lambda calculus with a leftmost-innermost evaluation strategy isn't "timeless" as you say, there is the possibility for the programmer to schedule evaluation, i.e., somthing like the Scheme "begin" form could be rewritten as lambda abstraction


(begin a b c) = (((lambda x (lambda y (lambda z z)) a) b) c)

Functional debugging

There are many debuggers for functional languages that take a stepping (explicit time) approach. There is a notion of time even in pure FP languages: in a big-step semantics it's implicit in the depth of the dependency graph, with a reduction semantics it's already explicit. See stepping debuggers for Scheme (e.g. Chang et al's "Stepping lazy programs") and for big-step/natural semantics see Chitil's HAT Explore, which animates execution traces in the form of Evaluation Dependence Trees. Algorithmic debugging tools do something similar too.

Da Silva in his PhD thesis (Correctness Proofs of Compilers and Debuggers: an Approach Based on Structured Operational Semantics) discusses how to derive a transition semantics from a big-step semantics, as I recall with the goal of deriving stepping debuggers which are correct w.r.t. some prior big-step semantics.

(It's a bit of a hasty response, so I'm afraid I've been rather lazy with the links. But I don't think you'll have a problem finding these and many other examples.)

No, it doesn't....

Imperative programming has an important advantage: it has a notion of implicit time [...]

This isn't true, unfortunately.

Basically, a method in an OO programs works by temporarily breaking the object's invariant to update it to a new state. However, if you call a method on an argument object while the current object's invariant is broken, you have to be sure that there is no path by which that method invocation can indirectly invoke a method on the current object. That is, you either have to ensure that method calls are not re-entrant, or ensure that all public methods are safe to call at any reachable broken state.

This means you no longer have a notion of sequential time under the programmer's control: it's a lot more like concurrency, where you have to accept that methods on other objects are executing on their own clocks, and reason about the possibility of interference from them. Basically, as soon as you have any form of higher-order state -- like objects -- essentially all the difficulties involved in concurrent programming are now present. (This is why thinking about objects as little computers that exchange messages is a really good idea, IMO.)

I think there's probably some mileage in exploiting some of the work on concurrency, like session types and deterministic parallelism, to invent new constructs for OO programming.

Isn't that an argument *for*

Isn't that an argument *for* functional programming? For a particular execution of a functional program we have a dataflow graph frozen in time. To visualize that we *can* use an imperative interpreter and display it as a linear sequence (which is a topological sort of the graph). The problem with that is that it introduces a lot of fake dependencies between different nodes in the graph: if we have f(a) + f(b) then we don't care about the execution order between f(a) and f(b), so we'd rather not display any dependency between them in the visualization. Aren't we in an even worse shape with imperative programming? There the fake dependencies are not only present in the way a particular interpreter executes it, but they are actually encoded in the program text!

For example, I think this:

  [1,2,3,4,5]
       |
  map (x -> x+1)
       |
  [2,3,4,5,6]

Is a much better visualization than what you'd get with what is presented in the article applied to this imperative loop:

xs = [1,2,3,4,5]
ys = []
for i in 0..xs.length:
  ys[i] = xs[i] + 1

You are right in theory. In

You are right in theory. In practice, data-flow abstractions like map are fairly opaque; they "magically" do a lot of stuff in one step that is difficult to reason about in a debugger. In the imperative world, I have the chance to inspect both xs and ys, and I know exactly what the computation is doing at each step in the debugger. In the functional world, there is just one step where a whole bunch of values are transformed. Replace "map" with a chain of transforming and filtering functions and the debugging problem becomes very bad.

Perhaps this could be solved eventually with better debugging technology, but I also wonder if there is something intrinsically more debuggable about control flow.

Programs that explain their work

I think the good question is not "at which step did that (unexpected) thing happen", but "why do I observe this (unexpected) result", said otherwise "which part of the computation lead to this part of the result".

Roly Perera made a fairly interesting comment in this discussion thread, but was too modest to mention his own work: Functional programs that explain their work, with Umut Acar, James Cheney and Paul Blain Levy. It's very nice, and extremely related to this question of "why did I get this part of the result?".

Isn't that a noble yet

Isn't that a noble yet elusive goal? Of course, the best kind of debugging is the debugging that doesn't happen at all because the computer can just tell you what the problem is. Today, debugging is a much more intensive process of experimentation and exploration.

Roly Perara's paper is on my to-read list (printed out even), but I always read ICFP papers very slowly.

Perhaps I should elaborate a

Perhaps I should elaborate a bit about how I think such a functional debugger could work. A functional program takes an input and computes an output. For example:

f xs = sum (map g xs)

Say the input is [1,2,3]. We can visualize that as:

[1,2,3]
   |
   f
   |
   9

You can click on the (f) to expand it:

[1,2,3]
   |
 map g
   |
[2,3,4]
   |
  sum
   |
   9

You can click on map to expand it (perhaps there is a different command to fully unroll the loop/recursion instead of just 1 step):

[1,2,3]
 | | |
 g g g
 | | |
[2,3,4]
   |
  sum
   |
   9

Or for the fans of a zoomable UI you could zoom in on a function node and as you zoom in you see the internal data flow.

Additionally when hovering with your mouse over a data item, you compute which other currently visible data items are affected by it and highlight them. For example if you hover over the 1 in the top list, the 2 in the bottom list lights up. You can then provide an option to explore the data flow graph between those nodes, in this case

  1
  |
  g
  |
  2

I have no idea whether this would actually be useful or whether in practice this would just turn into a messy graph with edges going all over the place, but intuitively I'd say this scales much better to real programs than simply displaying next to each variable definition all the values that that variable takes on in time. For small examples that works fine because it just shows everything, but for real scenarios what you hide is as important as what you show because while a human can spot the wrong value out of 10, he can't spot the wrong value out of 1,000,000. Zeroing in on the right function call is very important.

With what I sketched above you can do kind of binary search in the graph to find the bug. Presumably the input is correct but the output is incorrect. You keep expanding intermediate nodes, and if the input of the intermediate node is what you expect but the output isn't what you expect, then there is a bug in that node. If you do the timeline view where you just smash together this graph in a particular linear order, exploring it becomes much less efficient because now you've introduced a lot of artificial dependencies between what can affect what, vastly reducing your ability to prune the search space.

I really want to learn how

I really want to learn how to write like this. Really, this is how we should do PL papers, or at least PL descriptions.

I really have to let this sink in for a few days. There are a lot of good things to talk about; none of this is new but its really not the point.

Agreed

It's a nicely constructed polemic. I'd hope there's a sweet spot somewhere between this and a more rigorous academic paper.

You start with a big font, ego

If you're going mimic the presentation style, please omit the "everything before this is crap" vibe.

John McCarthy called this

John McCarthy called this something like "assassinating the predecessor" (in a talk he gave at an International Lisp Conference.)

Of course he could have made

Of course he could have made his opening points more diplomatically, but I don't think they are invalid. The big font is more for the typographic header effect, like an H1, I wouldn't equate it with ego.

Not about live coding

Bret Victor does not talk about live programming, although I understand that the community wants to claim him as a charismatic evangelist to their purpose. Note the footnote at the article:

This essay was an immune response, triggered by hearing too many times that Inventing on Principle was "about live coding", and seeing too many attempts to "teach programming" by adorning a JavaScript editor with badges and mascots.

Live programming is not an

Live programming is not an end, but a means to something els. Bret Victors is focusing on a goal with some techniques, one of which just happens to involve liveness, and a whole bunch of other things that are related to the augmented programming experience.

This is LtU, we don't need charismatic evangelists.

About live coding

If you follow the TOPLAP link, you'll find that the features he demos have a *lot* in common with those of live coding systems in the wild.. And some live coders who aren't very impressed.

About the features

Actually, "The features are not the point" is quite prominent in his essay. While many of those features do exist in the wild, that does not mean it will be easy to tame them or make them live harmoniously. It's a feature eat feature world out there.

Anyone who focuses on the features is missing at least half the point.

People from the live coding community, who indeed reach for more than a technical minimum for "live coding", should be grateful to have someone with a much greater reach speaking some of the same concerns they have.

Sure

But these shared objectives tell also of shared aims. Live coding researchers are driven by rich principles, including the principles of bricolage programming that Bret Victor alludes to, but with focus on programming as a shared, social activity.

But yes, I'm very glad that this polemic is challenging how programmers think about programming.

"Live coding" is a special

"Live coding" is a special activity that I can't see being very common. Its kind of cool, yes, as part of a performance of presentation, but they are not typical programming activities. Programming is a social activity like writing is. It is quite interesting to watch someone write, and there are even obscure but interesting activities where people write together. But...the typical programming activity will remain a solitary experience at least while the code is being written.

"Live coding" is a special

"Live coding" is a special activity that I can't see being very common.

I think it could be very common. What if more sophisticated scheduling apps could be available to business execs if only they had live visual programming of workflows in a UI DSL? How about integrating such schedules with your GPS unit when you get in the car?

All sorts of mashup-like scenarios could be common in the real world if only some sort of standard visual programming language were available. I think it would be difficult, but I'm not convinced it's impossible.

I've had this thought also,

I've had this thought also, actually, ever since I was into Cyberpunk/Shadowrun when I was in high school. I mean, ya, your "library" is prepared before hand, but when performing (a task), you would need to improvise according to an unforeseen situation.

"Live coding" is very common

I'd argue that live coding is the most common approach to programming. Spreadsheets are the classic example of live, end-user programming, people making spreadsheets that react to every change, and using them to model their world and communicate various visions of the future. Of course live coding is associated more with music than financial projections, but don't fall into the trap that professional programmers have the monopoly on programming in the professions. It just isn't so.

Of course we're writing together right now, and there is a huge shift towards live writing on collaborative documents... It's so much easier to collaborate on a document in google docs than via email attachments. And what's twitter if it isn't a social writing activity? Perhaps the greatest gift that the Internet has brought is social writing.

I thought that you defined

I thought that you defined live coding as a real-time social activity; it is in-situ performance vs. just getting live feedback. Even when we are editing our spreadsheets, we are doing so more often alone than as a group activity.

I can reply to your post whenever I want. I'm reacting to your change, but not in real time. I can edit my post, I can think about it, I'm under no time constraints to write it. No one is watching me write.

The difference between what I call live programming and what you call live coding is just the context of the experience. To me, live programming improves the solitary programming experience with live feedback, live coding is about in situ performing live with code, where live feedback definitely adds value. But they have fundamentally different goals. Is my understanding correct? Here is what I get from wiki:

Live coding (sometimes referred to as 'on-the-fly programming', 'just in time programming') is a performance practice centred upon the use of improvised interactive programming in creating sound and image based digital media. Live coding is particularly prevalent in computer music, combining algorithmic composition with improvisation.[3] Typically, the process of writing is made visible by projecting the computer screen in the audience space, with ways of visualising the code an area of active research. There are also approaches to human live coding in improvised dance.

There isn't an entry for live programming, this is a term I borrowed from Christopher Hancock's thesis.

Furthermore - what about education

In an educational context, social programming is near essential, otherwise students drop out, and you end up propping up the usual male stereotype.

Mark Guzdial says that live coding is best practice in computer science lectures, citing this.

I'm not getting your

I'm not getting your thinking here. I understand "live coding" and performance, performance could be a lecture, it could be a demonstration, it could be an operation. I just see the live feedback as being separable from live coding; one is a feature/technique, the other is an experience. Is that wrong?

Live Documents

There's a difference between writing live documents and collaborative writing. Though, both are social experiences. Live documents become social when my document can influence yours or vice versa. Live coding is a foundation for social experiences even if it is not performed collaboratively or in real-time.

I seek a day where programming becomes a natural means of expression, supporting rich and ad-hoc relationships among humans, businesses, and environments.

"Understanding programs" - ?

[...]operational semantics [...] forces people to think about programs in terms of computational behaviours, based on an underlying computational model. This is bad, because operational reasoning is a tremendous waste of mental effort.

E.W.Dijkstra, On the cruelty of really teaching computing science

cough cough bullpucky

if i understand the meme correctly, i think it falls apart as soon as you are going to use an api that is anything other than something core and long tested, long used. often we need to look at things 'operationally' because the code in question is some api that we just hacked up, and it has thinko bugs in it, and we can see them more easily as we single-step through.

Operational thinking

often we need to look at things 'operationally' because the code in question is some api that we just hacked up, and it has thinko bugs in it, and we can see them more easily as we single-step through.

I thought we were talking about learning to program, not about hacking. Operational thinking may help in certain situations, but it should not be the main (or only) way of thinking taught to students.

It's always how I ask

It's always how I ask students to debug their code: run it on a piece of paper on a small input. I found it especially useful for recursive functions, before the student got the big picture of thinking inductively of recursive calls as "supposedly doing exactly what I think they should do" while still in the middle of debugging the definition.

I'm not going to teach you

I'm not going to teach you how to teach (I myself don't teach any more), but I hope you understand that only very simple programs can be explained that way. The real problem we should be concerned with is the tremendous complexity of the modern software, and the operational thinking is not going to help manage it.

There is also that having

There is also that having the student simulate everything in their head is actually a big turn off to most students. This is where they drop the course and start to believe they have no chance of every learning to program. Ya, your better students, your computer science majors, should be able to do this relatively early. But I think, if we are going to open up programming to more people, that just showing them what operational thinking the computer is doing is a much better way to start.

Self-explaining computation

I think we'd do well to open up all our UIs, apps, and services - to the extent allowed by security policies. Curious users should be able to poke under the hood and observe what leads to particular states or conditions (or why the light box is all wrong). Not all users will be curious about all programs, of course. But pervasive access can raise the average computational literacy. And, if the computation models support valid intuitions (e.g. through compositional properties), the formal education should go much faster for someone who has had long exposure.

I'm not sure I'd call this "operational thinking the computer is doing", though. But it's more about self-explaining systems, x-ray goggles to see inside engines, etc..

Even with non-operational

Even with non-operational semantics you can support operational thinking. E.g. I could ask a student to trace updates in a spreadsheet, or represent evaluation of an applied function. Yet, non-operational semantics may better support other modes of thinking that are more simple, compositional, scalable.

Neither the Dijkstra quote nor rmovchan have expressed general opposition to operational thinking, only to it being a primary mode of thought. A non-operational semantics seems necessary if we are to reduce need for operational thinking.

I think there is benefit in encouraging developers to grasp small examples in a concrete, operational manner. I think this was your intention with your example. But, as noted, it isn't a very scalable technique, and may have a negative impact of perpetuating vicious cycles in our schooling, tooling, and HR.

Computing science

Note that Dijkstra wisely uses the term 'computing science' instead of 'computer science'. CS is about solving problems, not about computers. Computers are only the tool for solving CS problems.

Unfortunately, the way CS is taught (as I imagine - I'm not in the education since 1991, but I don't think a lot has changed) is more about computers. Students learn how computers perform simple programs, programming languages are taught in terms of their operational semantics, etc. They are made to believe that the imperative programming paradigm is - if not the only one - the most 'natural' and 'close to the computer'. Well, maybe it is so with the modern hardware, but it doesn't mean it should be and it will always be so. The education system does nothing but sets in concrete the mistakes and misconceptions of the past.

Computer Science

The original use of the word computer was an occupation, like accountant. I find that an amusing reminder that areas of computer science are as much about cognition, creativity, communication, and cooperation as about calculation. :)

I've sometimes called it 'computation science' and I can appreciate Dijkstra's calling it 'computing science'. With such tricks of speech we can subtly direct attention to the aspects that most concern us.

The education system does nothing but sets in concrete the mistakes and misconceptions of the past.

I sometimes have that impression, but it does not align with my own experiences. My study of CS at university included broad experiences with digital logic, discrete logic, functional programming, term rewriting, automated theorem proving, artificial intelligence, compilers and metaprogramming, operating systems, distributed systems, survivable networks, requirements analysis, software management. I believe we take from an education what we want from it, but it helps that courses were available involving those subjects.

While I did come away thinking that imperative is 'close to the CPU' (and I still think that), imperative is not especially 'close to the machine' with respect to digital logic, DSPs, FPGAs, nor even GPUs at the small scale. Nor is imperative good for networked systems at the large scale. Procedural programming is a firmly mediocre paradigm. It doesn't scale in any direction.

As long as the modern

As long as the modern programmable computer is based on flip flops, it will be biased to imperative programming. At a large (e.g. distributed) scale, other paradigms begin to become beneficial, but even then they are mixed with imperative at end points (e.g. mapreduce). Pragmatically speaking, purely using any paradigm (procedural or functional) is going to have a mismatch with the underlying hardware, so mixing is good.

As long as the modern

As long as the modern programmable computer is based on movement of electrons, it will be biased towards reactive dataflow programming. Paradigms other than imperative don't "begin" to become beneficial only at large scales. It's the CPU, not the flip-flops, that creates a bias for imperative programming. To elaborate "any paradigm" as "procedural or functional" is a severely false dichotomy. There are paradigms that aren't a mismatch for underlying hardware, especially with linear typed reactive and dataflow languages. Basically, I disagree with everything you just said.

(Are you using 'imperative' where you mean 'stateful' again?)

The instruction set of the

The instruction set of the CPU is based around load/store from/to registers/memory. Even the GPUs had registers and increasingly have store able memory with crazy memory models. Data flow becomes better than load/store over multiple machines, but before that you'll take at least a performance hit, if not a programmability hit (due to having to contort your computation as data flow when it's easier just to tell the CPU how to do it directly).

Stateful != imperative but at some endpoint there is imperative code to instruct the CPU, even if this code is hidden behind a language-based abstraction layer. This layer has a cost and often doesn't provide more than its worth, especially for small problems.

I think David is thinking

I think David is thinking beyond current CPU technology, to FPGAs and custom ASICs.

Sure, I've never programmed

Sure, I've never programmed a fpga without using a few registers in my design, but that was a long time ago in school.

I don't think he's arguing

I don't think he's arguing against registers as much as he is arguing against the instruction pointer (sequential execution). For example I think (not sure) that he would find a stream transformer that on each input outputs the sum of the elements so far just fine. In contrast to the imperative model, hardware is implicitly parallel everywhere, even though it is stateful.

Indeed.

I think (not sure) that he would find a stream transformer that on each input outputs the sum of the elements so far just fine.

Indeed.

Not every programmable

Not every programmable device that interacts with memory is a CPU. And not every program that interacts with state (even explicitly) is imperative. Every general purpose dataflow programming models (FRP, synchronous reactive, RDP, etc.) can model or manipulate state.

CPUs and centralized processing architectures are not essential. But, while CPUs exist, there will remain a bias towards imperative programming. And I expect CPUs will exist for a very long time, due to path dependence.

Sure, I totally agree that

Sure, I totally agree that programming is possible without explicit memory/register manipulation, but the fact that this is often the shortest path between what you want to do and what the hardware does is unavoidable.

Computers

Historically computers were designed to replace a human when performing simple sequences of steps called algorithms. That's the reason why they are biased towards the imperative programming. But it doesn't have to be always like this!

It's sad that the progress in computer science has not been sufficient to affect the direction of the progress in hardware and what we have today is just a more powerful version of the computer of the 50s. One of the reasons of this I guess is the constant hunger for, obsession with the performance. Perhaps now, when computers are fast, is the time for a change?

Performance is still a valid

Performance is still a valid concern. Even for a fast computer, inefficiency shaves off battery life. And latency still has a significant impact on what is feasible. A more significant issue is that "obsession with performance" is often short-sighted. Applications are bloated, non-compositional, non-extensible, and duplicate a lot of work. This leads to much inefficiency and latency in-the-large that might have been avoided if we accepted a little apparent inefficiency in-the-small.

For example, we can create languages whose applications don't pay for the application-layer features they don't use (e.g. those four layers of toolbars), but it may mean a little inefficiency in-the-small to ensure that components can be safely installed and removed at runtime. (And even that little inefficiency might be mitigated by a more sophisticated implementation.)

Performance is not a price we need to pay. If you approach the platform of language design with such a sacrifice in hand, you're probably too ready to make it.

The role of a computer

Computers have gone a long way from being just powerful calculators. But this is not actually relevant; we shouldn't care about how a computer works!

To a programmer, the abstract machine must be as abstract and transparent as possible; ideally, there should be no machine involved at all when we describe the semantics of a programming language. All that matters is that we have a non-trivial problem and the programming language must help us express this problem via a number of trivial tasks. The machine is best to be left to decide how (in which order etc.) to deal with those tasks. That's programming as I imagine it should be - as opposed to writing precise instructions that (hopefully) solve the original problem. Even in procedural languages, what a programmer wrote rarely matches what is in reality performed, unless an assembly language is used (due to optimization etc.). Do we care? No, as soon as the end goal is achieved.

Abstractions leak. We have

Abstractions leak. We have to care about how the computer works to some extent, like we have to care about how computers are connected together. Otherwise, our performance could be so far off.

I don't think we'll be able to forget the computer until we hit singularity, and at that point programming becomes irrelevant anyways.

Computation is Physical

Information is physical. It has mass, energy, inertia. Literally. Destroying a bit of information requires creating heat, in the real world. (Filters are a way to turn information into heat.) It also has these properties in a virtual sense - e.g. it is never free to copy or move information. If you're interested in the physical nature of computation and information, look into conservative logic or the Feynman lectures on computation.

Computation science is a mechanical science. Every valid computation specifies a machine (or a set of possible machines). A language is more analogous to material than tool, but every abstraction and its integration still has a physical nature. Naive, idealistic language designers might ignore the physical concerns, and the inevitable result is an unscalable programming model.

We can develop programming models where ordering of certain subprogram expressions is predictably irrelevant. Indeed, I favor such models - RDP, stone soup programming, constraint logic, multi-agent systems, and so on. But we cannot accomplish this in ignorance of the machine.

Are there any computer

Are there any computer architectures in use for which there is any correspondence between logical information and quantum information? I don't ever recall reading an assembler tip to prefer SWP over MOV because it generates less heat.

I've perused a few proposals

I've perused a few proposals for compute architectures based on conservative logic, mostly to support mobile computing. I don't know whether anything came of them; it isn't a field I watch closely. But I doubt they'll breach the regular markets due to path dependence (i.e. the "can I run android on it?" question).

For the x86 architecture, it'd be pointless to prefer SWP over MOV at the micro level because both are destructive (and even loading the instruction is destructive). Also, the theoretical heat associated with destruction of individual bits is still orders of magnitude less than what chips require to update a register.

An "assembler tip" to prefer SWP to MOV would be exactly the sort of short-sighted optimization that people would be wise to ignore anyway. There's no point in preserving information unless you already know how you're going to use it downstream. If you used linear types to control when information is destroyed, you'll perhaps find that the choice between SWP and MOV comes more naturally from the larger context.

We are pretty far away from

We are pretty far away from the theoretical ideal computer where the difference between SWP and MOV becomes an issue.

Performance vs correctness

Applications are bloated, non-compositional, non-extensible, and duplicate a lot of work. This leads to much inefficiency and latency in-the-large that might have been avoided if we accepted a little apparent inefficiency in-the-small.

I agree 100%!

Performance is not a price we need to pay. If you approach the platform of language design with such a sacrifice in hand, you're probably too ready to make it.

No, I'm not, I just want to set the priorities right. Would you prefer a language that, for example, allows you to ignore overflows and incorrect indexes, or a language that always enforces checking for such conditions?

An assumed dichotomy

Correctness is a nice property, but is very contextual. I consider many compositional properties to be more important than correctness. I do prioritize memory safety, which is necessary for object capability security. But object capability security is more about controlling damage than enforcing correctness.

Testing for error conditions after they might have happened is often an act with high cost and low reward. Better, I think, to prevent the error (via staged analysis or gatekeepers) or define things such that there is no error (e.g. tweaking `int` abstractions to formally specify modulo arithmetic) or to control damage (e.g. by pervasive object capability security, breaking on predictable lines like a fusebox) or even to recover from damage (reactive resilience properties, self stabilizing control systems). Avoid unprofitable branches and checks.

I would prefer a language where errors in one subprogram or plugin can be confined so they don't affect other important subprograms and plugins. When one light goes out, the rest remain on. This is about robust composition, not correctness.

Call it that

Better, I think, to prevent the error

Of course, but it's not always possible. The reality is, there will always be attempts to do things that are beyond the possibilities of a particular function. Checking for all the relevant pre-conditions may not be feasible, or practical. And formal correctness proofs will never be widely used, in my opinion.

or define things such that there is no error (e.g. tweaking `int` abstractions to formally specify modulo arithmetic)

well, that's not just cheating; enforcing a more complex than needed model will only add complexity to the code and therefore will lead to more bugs.

I would prefer a language where errors in one subprogram or plugin can be confined so they don't affect other important subprograms and plugins.

Absolutely. Such a language would have to have no side effects, for example.

there will always be

there will always be attempts to do things that are beyond the possibilities of a particular function

Sure. There are probably many functions that are never pushed beyond possibility, but for at least some functions, procedures, or behaviors I expect limits will be stretched. Which is why I listed three options on how to handle that possibility without checking for errors.

formal correctness proofs will never be widely used

It doesn't take "formal correctness proofs" to support safe use of partial functions. I think we'll see growing use of local proofs or dependent types to simplify syntax and avoid runtime checks.

enforcing a more complex than needed model will only add complexity to the code and therefore will lead to more bugs

If our integer types model modulo arithmetic, then representing this in the semantics is exactly as complex "as needed". The problem with integer overflow was due to failure to model the complexity that is observable in the interface. We could, of course, enforce a simpler model by using arbitrary-width integers.

Absolutely. Such a language would have to have no side effects, for example.

Forbidding side-effects is certainly a sufficient condition for confining damage, but it's far more draconian than necessary. I've already mentioned object capability security as a much weaker restriction that can achieve the same goals. (If you're unfamiliar with object capability security, I suggest Ode to Capabilities as an introduction.)

Model and implementation

If our integer types model modulo arithmetic, then representing this in the semantics is exactly as complex "as needed".

No, I believe our integer type models the set of all integers. Otherwise, we could not assume any of the integer algebra laws to apply (we do, don't we?). This is the implementation restriction that not all arithmetic operations can be successfully performed. The model is what's in our head, and it can be sometimes different to what the actual implementation does. I insist that the implementation must explicitly fail when it can't perform exactly as the model requires.

The first step to building a robust system is recognizing that any parts of it can fail. Systems that 'never fail' are most likely 'cheating' at some stage. I mean, they most likely produce different results from what they claim.

Forbidding side-effects is certainly a sufficient condition for confining damage, but it's far more draconian than necessary. I've already mentioned object capability security as a much weaker restriction that can achieve the same goals.

My views at the security issues are quite different. First, I insist that computations must have no side effects. All possible 'side effects' must be under control of the runtime framework. Adding a security model to the programming language is ... well, not necessary.

The model is what's in our

The model is what's in our head, and it can be sometimes different to what the actual implementation does.

And that discrepancy is a common cause of error. To avoid that source of error, you can either fix what's in your head (and the specified model, documentation, etc.), or fix the implementation. E.g. you could switch to modulo arithmetic, or switch to arbitrary width integers.

we could not assume any of the integer algebra laws to apply

A great many algebra laws apply just fine to modulo arithmetic.

The first step to building a robust system is recognizing that any parts of it can fail. Systems that 'never fail' are most likely 'cheating' at some stage.

This advice seems naive. If we allow "any part" of the system to fail, we'll have much more difficulty reasoning about partial failure. A wiser approach is to control where failure is admitted as a possibility. You seem to assume a false dichotomy between "any parts of it can fail" and "Systems that never fail". In between is "some parts of it can fail", and systems designed to fail quickly or fail cleanly at specific times or places.

You've used the word "cheating" a few times in this discussion. I'm baffled on how "cheating" can even be applied in context.

I insist that computations must have no side effects. All possible 'side effects' must be under control of the runtime framework.

That may be your preference and your design. But it is not essential for a goal of limiting damage from errors (or even malicious code). People who want side-effects can have them and still develop systems "where errors in one subprogram or plugin can be confined so they don't affect other important subprograms and plugins".

Failure

To avoid that source of error, you can either fix what's in your head (and the specified model, documentation, etc.), or fix the implementation.

No implementation can avoid this problem, because of the infinity of the integer set. Fixing it in the head means that a more complex model will be enforced, and this will increase the likelihood of bugs.

This advice seems naive. If we allow "any part" of the system to fail, we'll have much more difficulty reasoning about partial failure. A wiser approach is to control where failure is admitted as a possibility.

This advice seems naive. If we try to control where failure is admitted we will surely miss a lot of places where failures can occur. By the way, switching to arbitrary width integers doesn't guarantee that arithmetic operations will never fail; you still can get, for example, out of memory errors.

I'm baffled on how "cheating" can even be applied in context.

I thought I explained what I meant by 'cheating'. Maybe it's not the best word to use here (sorry, English is my third language). I meant that systems that never fail most likely give wrong results under some circumstances, which is much worse than explicitly failing.

No implementation can avoid

No implementation can avoid [issues representing integers], because of the infinity of the integer set.

That's often an irrelevant infinity. There are many programs where you could statically compute the maximum size of any integer, and many more where you could place an asymptotic bound on it (e.g. O(ln(T))). But, that aside, it's a fair argument against integer types to note that they can't be done correctly in general.

Fixing it in the head means that a more complex model will be enforced, and this will increase the likelihood of bugs.

Fixing it in the head means that a complex model that is already impacting programmers will be better documented and understood. I do not see why this would increase the likelihood of bugs.

switching to arbitrary width integers doesn't guarantee that arithmetic operations will never fail; you still can get, for example, out of memory errors

If you compute the arbitrary width statically, you can guarantee against memory errors. If you can place a logarithmic asymptotic bound, you can easily guarantee against out-of-memory errors for a certain period of time (e.g. "won't run out of memory for 30000 years"). And, even if you admit memory errors, you can control when and where they are exposed (e.g. by developing languages that allocate and operate in well defined stages). For real-time control systems, controlling when and where memory errors are admitted is quite relevant.

If we try to control where failure is admitted we will surely miss a lot of places where failures can occur.

I don't know of any reason why that would be true. As a counterpoint, however, there are systems for proving code, even including resource costs (e.g. with linear logic).

systems that never fail most likely give wrong results under some circumstances, which is much worse than explicitly failing

Every system is part of a larger system. We can create many subsystems that won't fail. That does not mean every subsystem can be modeled without failure.

Regarding your assumption that providing a wrong answer is "much worse than explicitly failing" - that is itself circumstantial. We can create systems where even incorrect answers is mostly right (error bounds) or self-correcting (eventual consistency; improving values; meta-stability; learning models). We can create subsystems that deal well with limited errors in their inputs. Error tolerance and resilience is another approach to robust systems.

Reliability

it's a fair argument against integer types to note that they can't be done correctly in general.

No, it's an argument against the belief that some solutions will always work. What I'm driving at is that virtually any code can at some stage fail.

Fixing it in the head means that a complex model that is already impacting programmers will be better documented and understood. I do not see why this would increase the likelihood of bugs.

Wrong. It means that the specification of the system will be modified to reflect the implementation limitations, that's all. The likelihood of bugs increases because we replaced our intuitive model with a more complex model that deals with the limitations of a particular implementation.

We can create systems where even incorrect answers is mostly right

Of course. I only insist that any subsystem that failed, instead of just giving an incorrect answer, must inform the client/outer subsystem that it failed and the results are undefined, that's all. I believe it would change a lot if we could consistently implement this requirement; software would become a lot more reliable, even without resorting to the complex stuff like you mentioned.

Modes of failure

What I'm driving at is that virtually any code can at some stage fail.

This is only true in the sense that sometimes the machine will fail to faithfully execute the program ("hardware error") and your programming model does not robustly handle that case.

Not only

The integer overflow is not a hardware error, although it certainly can be implemented as a hardware exception. Out of memory is not a hardware error. Do I need to continue?

your programming model does not robustly handle that case

The point of my programming model is building robust software out of parts that can fail. The model is based on 1) subsystems correctly reporting their failure to perform their tasks and 2) Boolean logic-like code composition rules.

Not Sequitur

How are integer overflows or out-of-memory errors relevant to Matt's assertion? We can write software that won't ever have those errors. We can't write software that renders our hardware immune to cosmic radiation or thermal noise flipping bits in a register, wire, or memory.

I doubt your Aha! programming model can robustly handle hardware errors. Have you run a scenario where random bits are flipped at runtime?

I have doubts for whether Aha! will actually help for robust software. I feel you tackle the unnecessary errors (those caused by naive, optimistic abstractions) and ignore the essential errors (those caused by communication, external services, side-effects, concurrency).

Hardware errors

I doubt your Aha! programming model can robustly handle hardware errors.

It doesn't, but it doesn't mean it can't. External errors, like any external communication, are supposed to be handled via the API.

I feel you tackle the unnecessary errors

Most errors caused by improper coding are 'unnecessary'; if you don't believe eliminating them would help for robust software, I have nothing more to say. Those errors you call 'essential' are, in fact, not errors, I'd call them 'conditions'. Handling them is beyond the scope of a programming language design.

Note that failure in Aha! is not necessarily an error, it's a more universal concept.

Most errors caused by

Most errors caused by improper coding are 'unnecessary'; if you don't believe eliminating them would help for robust software, I have nothing more to say.

I do believe in eliminating errors from many subprograms. You're the one who seems to believe that eliminating errors from subprograms is impossible.

Those errors you call 'essential' are, in fact, not errors, I'd call them 'conditions'. Handling them is beyond the scope of a programming language design.

PLs are designed to solve real world problems. That you think handling robustness issues ('conditions') from the real world is outside the scope of PL design is quite telling.

External errors, like any external communication, are supposed to be handled via the API.

Hardware memory errors are not experienced as "external errors".

What problems?

PLs are designed to solve real world problems.

Of course. So are APIs, frameworks, etc. You can't have one solution to all real world's problems. You shouldn't design an air-conditioner that cooks dinner and entertains you, although these all are 'real world problems'. PLs need to first solve the problems created by themselves, such as poor code maintainability, poor error handling, poor scalability, etc. If my language does only one aspect of the many well, I'd be happy. The loss of focus in the PL design is one of the reasons of what we have now.

Hardware memory errors are not experienced as "external errors".

'External' means that they are not part of the computations and are (at least, partially) out of the program's control. Aha! has a distinct boundary between the immutable computation space and the mutable external world where all the 'side effects' occur.

PLs need to first solve the

PLs need to first solve the problems created by themselves

I agree with that. Yet, they cannot do so at the expense of addressing real-world concerns. I'm not suggesting a PL come with built-in features for printing 99 bottles of beer on the wall (cf. HQ9+). But if your language "does only one aspect of the many well", it's unusable.

Aha! has a distinct boundary between the immutable computation space and the mutable external world

Even so, mutation of the immutable computation space will be part of any Aha! implementation. Just because you're using some memory as an abstractly immutable space doesn't make it immune to bit-flips.

I agree with

I agree with that.

Hooray!

if your language "does only one aspect of the many well", it's unusable.

Any PL is unusable without the appropriate infrastructure: libraries, frameworks, IDEs, ...

mutation of the immutable computation space will be part of any Aha! implementation

Fortunately, this will be (or should be) completely transparent to the programmer.

Any PL is unusable without

Any PL is unusable without the appropriate infrastructure: libraries, frameworks, IDEs, ...

A language that "does only one aspect of the many well" is unusable even with libraries, frameworks, and IDEs.

Fortunately, this will be (or should be) completely transparent to the programmer.

Visibility to programmers seems off-topic to the earlier question about whether a programming model is robust to hardware errors. I get the impression that you've lost track of your point.

A language that "does only

A language that "does only one aspect of the many well" is unusable even with libraries, frameworks, and IDEs.

In that case, most, if not all, of the existing languages are 'unusable', according to you. Which may be partly true.

In my own opinion, Aha! is excellent in more than one aspect. I'm just waiting for people to confirm this. Which is hard to expect before the language is implemented...

Visibility to programmers seems off-topic to the earlier question about whether a programming model is robust to hardware errors.

I didn't think it's related. Please, be tolerant.

My programming model, I believe, is less vulnerable to hardware errors than some others, due to the universality of the concept of failure.

Failure

My programming model, I believe, is less vulnerable to hardware errors than some others, due to the universality of the concept of failure.

I'm sure that your model is less vulnerable than some others (phrased another way, it's not the worst model in existence), but that's not saying much! I'm not particularly knowledgeable about recovering from hardware errors, but two approaches that would seem to work are redundancy (duplicate memory state, logic, etc. at the lowest levels of hardware) and compartmentalized failures (preferably with compartments living on different hardware).

Aha! already has a form of compartmentalized failure, right out of the box, but IMO it's not a good fit for hardware error recovery. For one, hardware errors are likely to break invariants of your implementation, leading to errors that aren't simply handled by your failure handlers (e.g. pointer corruption). For another, the compartments are too fine grained for them to be good at recovering from hardware errors. Aha! can't spot the difference between failure as ordinary control flow and failure due to serious invariant corruption, so the latter will just propagate up one level of computation and then be misinterpreted as control flow (as opposed to a better approach of failing a larger compartment to restore the invariant).

You should consider this because, even though hardware errors are relatively rare, out-of-memory errors are not and it sounds like you intend to treat those by injecting failure into whatever computation needed the memory. That is going to complicate logical reasoning about Aha! programs, since you will no longer be able to assume in an alternative that the originally attempted condition didn't hold logically.

Good point

I suppose these errors should be treated as 'external' and therefore handled at the 'global' application level. I imagine this as a callback that receives all the necessary information and returns an action (i.e. a change in the application state). The reason for this is that external error handling is a separate aspect of an application and therefore shouldn't be injected into the application logic (throughout the entire source).

No, it's an argument against

No, it's an argument against the belief that some solutions will always work.

Why do you believe an argument that some solutions won't always work is sufficient to argue against a belief that some solutions or subprograms will always work?

The likelihood of bugs increases because we replaced our intuitive model with a more complex model that deals with the limitations of a particular implementation.

I don't believe that addressing limitations of implementation is necessarily more complex or less intuitive. Nor does it need to be limitations of a particular implementation; it could address a whole class of implementations (that is, limitations, resources, and error modes can be abstracted).

Also, I disagree that the model is "more complex" than the programmer already had to reason about. You're trying to argue for WRONG INTUITIONS instead of a more accurately complex model. I expect the likelihood of bugs increases if we encourage developers to hold invalid intuitions.

software would become a lot more reliable, even without resorting to the complex stuff

Consistently reporting error when it occurs is useful when feasible, and is not something I've argued against. But it does not improve reliability to design systems where every subsystem may fail. And reporting failure does not, by itself, improve reliability of a system - you need to actually act based on that report (fallback, recovery, tolerance). After you've addressed the burdens of propagating, analyzing, and handling error reports, your solution will be no less complex than those I favor.

Wrong intuition

You're trying to argue for WRONG INTUITIONS instead of a more accurately complex model

We were talking about modelling integers, right? I suggested that instead of changing our programming model to work with the modulo arithmetic (that never fails) we can keep thinking of integers as an infinite set but allow some operations to fail. I said this would greatly simplify the model because we could apply the familiar laws of arithmetic. Otherwise, our model would have to be a lot more complex and would require a lot more error-prone coding. For example, we would have to explicitly check if adding one to a positive counter returns a negative number. Failing to do so could result in severe consequences, possibly without us knowing it.

I hope you didn't mean to say that abstraction is always 'wrong intuition'.

reporting failure does not, by itself, improve reliability of a system - you need to actually act based on that report

Absolutely. That's exactly what the Aha! composition rules are designed to do.

We were talking about

We were talking about modelling integers, right?

We were talking about modeling the `int` types used in a program.

I hope you didn't mean to say that abstraction is always 'wrong intuition'.

Abstractions that require implicit failure when used are wrong. When we intuit from such abstractions, we form wrong intuitions. The notion of `an infinity of integers` is one common example of a wrong abstraction that leads to intuitions that are wrong in the physical world. (Irrational real numbers are another.)

we would have to explicitly check if adding one to a positive counter returns a negative number. Failing to do so could result in severe consequences, possibly without us knowing it.

We could address this formally by representing a finite integer abstraction (as opposed to modulo arithmetic). Or we could know and control the consequences of failure.

Either, I think, would be better than failing at some unpredictable, implicit 'condition' that is not understood or expressed by any abstraction in the code (e.g. out of memory).

Model of a model?

We were talking about modeling the `int` types used in a program.

Wow, I didn't know that we need to model that. I thought 'int' itself is a model of the integer number.

intuitions that are wrong in the physical world. (Irrational real numbers are another.)

To me, irrational numbers are the right abstraction. It looks like you, like the ancient Greeks, don't believe in their reality. The infinity of integers is not 'wrong abstraction' either. Do you think that 'right abstraction' is something that can be expressed as a string of bits? I don't understand.

We could address this formally by representing a finite integer abstraction (as opposed to modulo arithmetic).

Do you seriously believe this would change anything?

Either, I think, would be better than failing at some unpredictable, implicit 'condition' that is not understood or expressed by any abstraction in the code (e.g. out of memory).

What's so scary about failures? Isn't it because the languages you learnt so far handle them poorly?

Models of Models are common

Wow, I didn't know that we need to model that. I thought 'int' itself is a model of the integer number.

`int` is whatever its defined behavior happens to be. Whether you think of it in your head as an integer or modulo arithmetic or a dream of the Red King is quite up to you, but not all 'head models' are equally rational.

More generally, we often model models. What do you think an `integer` is in the first place?

To me, irrational numbers are the right abstraction.

Whether irrational numbers are a right abstraction is not a matter of taste or preference. But I can say this with certainty: you've never used or encountered an irrational number in your life (by which I mean: outside of abstract realities postulated for math problems).

Do you think that 'right abstraction' is something that can be expressed as a string of bits?

No. That would be an oversimplification. A 'right abstraction' is one that keeps its promises when you actually use it.

Do you seriously believe this [formal, finite integer abstraction] would change anything?

Yes. It would offer deterministic behavior, subject to testing or redundant computation. It would support hard real-time guarantees, effective and valid static analysis, and strong intuitions about failure modes.

What's so scary about failures?

The number of ways things can go wrong is much greater than the number of ways things can go right, and consequently much more difficult to reason about. Partial failure - where part of a system fails, and the remainder must cope - is one of the big unsolved problems in computer science. It's especially prominent in distributed systems. We write our programs to impose some useful order on our world, and we really need all the help we can get.

Failure is also a small aspect of a larger problem related to computer security and systems survivability - integrity of services, graceful degradation, resistance to denial of service attacks, etc..

Isn't it because the languages you learnt so far handle them poorly?

Well, they'd certainly be less scary if languages handled them well. Even Aha! handles such a small slice of the problem as to be irrelevant for real world considerations.

[edited at some point between rmovchan reading and responding.]

anyone who says "to me"

anyone who says "to me" before a sentence that should be judged objectively should not be taken seriously

Thank God you don't take me seriously. Do you?

So, how do you judge which abstraction is right and which is wrong?

The number of ways things can go wrong is much greater than the number of ways things can go right, and consequently much more difficult to reason about.

Do you think that if we pretend that things go right, our problems will go away? I think many bugs are caused by our reluctance to admit that things not always 'go right'. Of course, it's much easier to always assume the best scenario and ignore any deviations from it - until things completely fall apart.

Failure is also a small aspect of a larger problem related to computer security and systems survivability - integrity of services, graceful degradation, resistance to denial of service attacks, etc..

I look at the concept of failure much broader. To me (forgive me this 'to me'), failure is not like an exception in other languages; it's any situation when a computation didn't achieve its goal. There's no separate 'normal' and 'abnormal' computation logics, failure is as normal a result as success, except that any outputs are discarded and can't be used.

Do you think that if we

Do you think that if we pretend that things go right, our problems will go away?

Of course not. I have not advocated any such pretense. I also do not advocate use of abstractions that would require any such pretense.

failure is as normal a result as success, except that any outputs are discarded and can't be used

That's a pretty big 'except'. Your intention to discard outputs seems to be why your Aha! utterly fails to address the majority of system failures (where computations involving communication, concurrency, or incremental manipulation of state may fail to achieve their goal).

I prefer to model errors explicitly, as needed, and leverage staged programming (monad transformers, arrow transformers, user-defined syntax, or similar) should developers wish to partially separate expression of error handling. I prefer to model failure as a choice (e.g. `Either` types), not an exception.

Your intention to discard

Your intention to discard outputs seems to be why your Aha! utterly fails to address the majority of system failures (where computations involving communication, concurrency, or incremental manipulation of state may fail to achieve their goal).

I can't see where Aha! failed yet.

In my programming model computations don't involve any of that, it's the other way around. E.g. communications involve computations. If such a computation fails at some stage, the communication breaks, and the error handling routines are called. None of the communication up to this point will disappear, of course.

I'm not fond of exceptions. I prefer to model errors explicitly

Neither am I. I don't even use this word. Failure is a much more useful concept. The reworked Getting Started guide on my site gives a good insight into failures.

I can't see where Aha!

I can't see where Aha! failed yet.

I see what you did there.

None of the communication up to this point will disappear, of course.

And since any computation can fail, communication can fail at any intermediate state, leaving the greater burden of partial failure to your developers and their error handling routines.

Failure is a much more useful concept.

I think that whether failure in Aha! is more useful than error in some other language is more an issue of how the failures or errors are handled (and what guarantees you can easily make) than of the underlying concepts of failure vs. error.

since any computation can

since any computation can fail, communication can fail at any intermediate state, leaving the greater burden of partial failure to your developers and their error handling routines.

The language itself doesn't specify how communications are implemented; this will be part of the API. This is still work in progress; I will publish the API once it's more or less finalized.

I think that whether failure in Aha! is more useful than error in some other language is more an issue of how the failures or errors are handled (and what guarantees you can easily make) than of the underlying concepts of failure vs. error.

"What makes failures useful in Aha! is its computation logic: a failure never breaks the control flow, never leaves variables unassigned, never causes 'panic'.".

Perhaps now, when computers

>Perhaps now, when computers are fast, is the time for a change?

Nope, as any hardware change is very expensive, correctness/security improvement(*) don't/won't happen..

*: such as instructions which trap on integer overflow, segmentation, etc

Oh really?

I wasn't talking about a change in hardware (yet). I thought it's undeniable that hardware gets cheaper and cheaper, so maybe it's time to change the attitude, to set the right priorities: correctness and security over performance, for example. It will be damn hard, given the mentioned 'obsession with performance'. There's an illusion that security and even correctness can be sacrificed for the sake of performance; in reality, this makes even more harm to the performance. I'm convinced that software that neglects security and correctness at any stages of the development ends up being bloated and inefficient.

Power efficiency is becoming

Power efficiency is becoming more important, so you can't get too far off on performance. A static language can be more useful in this regards, but it also depends on how often the code run (is the code just gluing together efficient components that do most of the work anyways?).

Correctness also has little to do with security unless security can somehow be formalized and verified, but often, its just a matter of design and high-level policies that are hard to specify very clearly in any case.

And the point is - ?

Sorry, I didn't get your point. Note: I didn't mention the formal correctness, I mean just correctness in the general sense, just making your code do what's needed, reliably. Attaining the sufficient (maybe not maximum) performance can be considered a part of it.

I wish

Look at the ARMv8 ISA, I think that it's one of the most recent new CPU ISA, do you see anything in this ISA related to security/correctness?
I didn't! It's all about 64 bit and the ease of implementation for better performance&less power usage.
I agree with you that hardware support of security mechanism (for example integer overflow detection, segmentation..) is much more power-efficient than doing it in software, yet as shown by the ARMv8 case, this isn't really happening.
I would say that the last real advance on security are virtualization and IO-MMU, they are integrated in hardware but not for security reasons..

Virtualization

Note that virtualization is a very old idea, it was implemented on mainframes as long ago as 1970s...

Live coding

Live coding is about bringing code into live interaction, not a single technical implementation of interactive programming. Therefore all Bret writes about is in fact about live coding.

What limits Bret’s argument is the focus on lone programmers interacting with their code. Live coding (and indeed creativity) in general is much more about liveness in terms of social interactions, including in live performance. It’s this social context of live coding, much more than visualisation on its own, that has been proven to be so beneficial to computing education:
http://dl.acm.org/citation.cfm?id=1047482

Very interesting article.

Very interesting article. The examples all fit in the intersection of the following three categories:

  1. Painting simple shapes on a 2d canvas,
  2. working with integers,
  3. with a tiny amount of code with at most a single loop.

Predictably, these kinds of visualizations work very well for this intersection. For learning programming, this is certainly a good start, but at the end of the document the author presents this as a solution for general programming by people who already know how to program. For that to come true, this has to escape the tiny niche in which it is presented here. It should:

  1. Go beyond domains like painting simple 2d shapes that are already visual by nature.
  2. Work for arbitrary data such as strings, lists, trees, and domain objects.
  3. Work for code bases larger than 10 lines, with a lot of functions that call each other, and control flow resulting from higher order functions (or objects).

Even within the tiny and homogeneous intersection that the article is about, the author needed a different mini-IDE for each example. So if you want it to work for a larger domain, you certainly need the tools to be customizable in a cost effective way (the time you save by having such a problem specific tool should be greater than the time you spend customizing the tool).

Gimmicks

Even within the tiny and homogeneous intersection that the article is about, the author needed a different mini-IDE for each example.

I agree many of the techniques don't generalize. Even within graphics applications, how do you render a time lapse of your GUI application with transparent overlay? Still, working in an environment where you can easily install and interact with your own custom visualizations would be pretty nice. I think you should imagine him having skipped a step where the programmer first selects a visualization.

Right. The challenge is how

Right. The challenge is how do you let the programmer choose such a custom visualization? For many things you'll want to implement your own. You also need tools to weed out the noise. With billions of iterations it's hard to get a full picture. For example if you have nested loops, you'll want an option to hide the substeps in the innermost loop, and instead consider a full run of the innermost loop as a single step. Another challenge is how to visualize code with function calls. Suppose you have a function definition, and many calls to the function (possibly recursive). Then visualizing a variable used inside that function just by displaying all its values over time as a linear sequence is not very helpful. You want tools to zoom in on a particular call. If the call tree in a particular run has a billion nodes, that makes finding the right one challenging. The next challenge is that the data in a single variable itself can be huge, for example a binary search tree instead of an integer.

I do definitely think that this is going in the right direction, but we're not almost there and we don't "just need to take off our blindfolds" as the author says.

The return of printf debugging

Exactly. My current idea is that this should be under programmer control, that the programmer is responsible for programming the debugger UI just as they might poke around an executing program using a REPL. So the first step is to project source code on a trace of the program's execution, then you write code in each function to "call attention" to certain execution points with console statements. The program runs, the console is populated, and then...you can click on these statements to go back to points in the execution (which we achieve by re-executing, with selective memoization to make that fast).

that reminds me ...

Sean McDirmid: the programmer is responsible for programming the debugger UI just as they might poke around an executing program using a REPL.

That matches my line of thinking, which I can outline in the context of a never-started personal project I've been imagining a couple years now. (When I mention lightweight thread systems and rewriting C, etc, it's about this idea.) The main idea involves how one can test something hard to test because it's a headless async distributed plugin. I play video games instead of doing this though, in case you're curious. :-) Except I can't quite ignore it completely.

(The short version of the following goes like this: I want to know and control what actually happens in distributed network components when I test them. And doing that well is so hard that any means you use to perform such tests would also work well as a library for writing such components—if you weren't afraid they had the same bugs. My main reason to write this is to make someone think, "That gives me an idea.")

Here's terminology and names I'll use, made up just to post this. Say P is short for Plugin: the irritating thing you want tested really thoroughly via some test T, so you have confidence P will actually work when deployed in E, for Environment. Each uppercase letter is a big hunk of code, and we write them adjacent to one another when used together inside one process. Thus PE means P in the E environment process, and PT means P in the T test process. Lowercase is the interface seen by other code, so E sees p as the interface of P, and P sees e as the interface of E. Separate lowercase api names might simplify otherwise very confusing analysis.

Now environment E is very complex, on the order of hundreds of man-years of effort, so the interface e exposed to P is (alarmingly) complex in it's own right. The P library also has to craft interface p so it not only suits E, but also stand-alone test T and other hypothetical clients C as other endpoints that might talk to E as well as each other. Thus plugin P defines an abstract api c for generic C client behavior, then connects the t api of T to this, as well as the e api of E. This might make you nervous already, before I even note the allowed range of timing behavior of E is enormous, as a network component, which means simulating this range in test T is impossible unless T is much more complex than plugin P itself. (I've gotten used to the idea it's harder to test a library than to write original code.)

Currently E, P, and T are all a bunch of C code of teeth-grindingly low level. I want to expand a framework for T, using several new capital-letter-named components, so it includes a language L (making it more relevant here) as well as other neat toys to make life easier, and make useful simulation practical, realistic, and (and related to your post) debuggable.

Instead of T, I want a cluster of tools YFLWSD (only coincidentally reading y-flows-d) in daemon D that can be browsed and driven by browser B using web-server W inside the daemon. S here is a shell for command lines, providing an alternative channel outside HTTP supported by W. L is a language for writing scripts, and can vary as needed, but I was thinking of writing a Lisp on top of YF, where Y is yet-another-low-level-C-only-framework, and F is shorthand for Fibers in a lightweight thread engine and scheduler. Something like F fibers is needed to simulate real network applications operating on thousands of connections in one process. A language L using them would have a builtin async threading model, which is just nice but no big deal.

Ordering is roughly lower to higher level, like a left to right layer cake, so typically names to the right depend on names to the left. To test plugin P, we stick it in the left-most position, PYFLWSD, so it's driven by a Y library in F threads in language L exposed by web server W and shell S hosted by daemon D, where you run as many D daemons in as many locations as need to simulate desired conditions, or permit other tools to capture and analyze network traffic. A main purpose of W is debuggability via browsing data you want to expose.

If a plugin P is sufficiently complex, and environment E is sufficiently hard to simulate, you might have one process instance of PYFLWSD devoted just to driving P while it talks to other distributed P instances (presumably over a protocol shared by E and daemon D). So we might want to run a connection through two daemons in series, with an extra instance of YFLWSD in front as a proxy to focus on building a picture of everything happening in PYFLWSD. This would let you devote serious resources in the proxy to model analysis, history managememt, and traffic capture without seriously disturbing what happens on the PYFLWSD process driving plugin P. (That would matter if performance was an aspect of what is tested.)

In this model, a browser acts as debugger UI which a programmer carefully programs in the daemon, along with the simulations driving whatever a test must do. This might sound like an absurdly complex want to test plugin P, but I assure you test T in PT is already absurdly complex, but not nearly adequate, and is already too hard to change further. The cheap and dirty cobbled-together T in PT doesn't test PE well enough and has reached a deadend.

Also, P looks long in the tooth (hand-forged FSMs aren't fun at all) and could be rewritten via YF with nice-looking lightweight threads in direct style, then tested for equivalance to P using the same daemon setup. It's too hard to debug P now directly; it can only be done indirectly via test, and only if the test reasonably simulates the range of behavior in environment E, which a current test doesn't. I hope this highlights why programming a debugger can be important in shipping software.

Edit: for clarity, let's use H=host instead of C=client, to reserve C as the programming language name in further discussion.

PET theory? :)

PET theory? :)

Pluggable Overlay Networks

My PL studies from about 2003 were focused on supporting overlay networks as language abstractions, with an emphasis on open systems, security, resource management, independent development, and survivability. I had many motivations for this: performance, scalability, disruption tolerance, extensibility, and expressiveness. The ability to simply add a new peer-to-peer network layers (without hand-modifying every application, and without strongly distinguishing client vs. server) is incredibly powerful and valuable.

Reactive demand programming crystalized from those efforts (from some obscure confluence of concerns involving resource management, live programming, and automatic content distribution to resist flash crowds and DDOS), and now greatly surpasses my prior efforts (including models based on mobile agents, actors, kell calculus, distributed state). A 'behavior', the basic programming construct in RDP, is essentially a pluggable overlay network (composable, too!).

Among my early motivations was one related to the current discussion: visibility, debugging, administration. The ability to observe a bunch of mobile agents would benefit greatly from an ability to inject observer code into a distributed system.

P sees e as the interface of E. E is very complex, so the interface e exposed to P is (alarmingly) complex. P library also has to craft interface p so it not only suits E, but also stand-alone test T

Towards the end of your post, I get the impression that your P is something very specific and not a generic 'Plugin'. But I'll answer as for generics.

First, I suggest you simplify `e` as seen by `P`. Consider using the object capability model and avoid entanglement. This will make it much easier to mock up an environment for testing, and to reason about effects by P.

For the interface `p` exposed to E, I have been favoring resource dependencies, plus hard and soft constraints. I describe a bit of this as stone soup programming. I've designed a version of this for Sirea's plugin system, using Haskell's incredibly convenient Data.Typeable.

plugin P defines an abstract api c for generic C client behavior

In general, you'll want to avoid any direct interaction between P and C, to avoid coupling, entanglement, and those ever painful startup-order dependencies. However, P can still provide an API that is indirectly accessible by C. One option is to use intermediate state (a blackboard metaphor). The client can know to write that state to achieve some effect from a plugin compatible with P. Another option is to use a service registry: P publishes interfaces (and metadata) to an intermediate broker, where C can find them.

range of timing behavior of E is enormous, as a network component

While the physical timing can vary widely, use of temporal semantics can regulate the failure modes: either deterministic logical timing or clean disruption (if the real-time constraints cannot be met). It's easier to reason about systems with fewer failure modes.

Bloom, Datomic, RDP, and Lightweight Time Warp each use explicit logical time in part to support distributed systems.

all a bunch of C code of teeth-grindingly low level

It is worth trying a higher level implementation, first. You'll save a lot of teeth, the performance will probably be better than you imagine, and a second implementation goes much faster anyway. Consider ATS or Rust for the low level.

a browser acts as debugger UI

That sounds good. I'm quite interested in developing web-based IDEs, compiler-as-a-service, etc.. The @unhosted efforts make it more interesting.

more detail

I wanted to respond to McDirmid's point about programming a debugger, which can be helpful in figuring out how code works. Part of an idea I had seemed related, so I described it hoping it would give someone ideas. My agenda ends after hoping to prompt ideas, so I don't mind if you pitch your stuff. I can expand on detail, but the story may only get more blurry; leaving out suitable detail is part of writing clearly. The one letter codenames were temporary throw-aways, probably less useful in a longer discussion. I should have used H for host, instead of C for client, since otherwise it's hard to talk about the C programming language. So let's edit my first post to replace C=client with H=host, thus reserving C as programming language name.

P is something specific I've been doing (design, coding, test and maintenance) several years for my employer, and of course it really bores me now. As I noted, it's hard to test. I'm professionally obligated not to describe it (much) unless given leave to do so. Environment E is something specific too. Only small parts of P are still in C++, while most is C now, as a project requirement. Choice of another language besides C is not an option. Also, memory must be statically allocated, and libraries cannot be used with dynamic allocation, native threads, or unknown behavior. The api of P is mostly queue-based, but getting specific about it would cross the line.

(At times folks have suggested, "Just write directly to the e api of E, and discard this abstraction nonsense." But when I ask, "Did you want the tests to stop working?" they change their minds, realizing there would be no test outside E if I did as suggested.)

Suppose I wanted to rewrite P in Rust. Folks would just laugh, because both api and implementation must be C with static memory allocation. I could rewrite it in C as if threads existed (lightweight thread fibers as per F), and then run it through a compiler doing CPS transform to make it look more the way it does now, which I've been doing manually and without lightweight threads. That way, it would be C before and C after, and the C folks would be able to grasp and audit both, largely making everyone happy. But that's neither about debuggability nor live coding environments, and slowly veers off topic.

Object Algebras

I agree that this should be in the hands of the developer. Though, I think we should make the question more difficult by asking: which developer? and will we often want more than one concurrent view? and how often do we want to change views?

If we tackle the problem from a more general perspective, I think we'll be able to find answers waiting for us. In particular, I believe that syntactic abstraction through Object Algebras are almost a perfect fit for the purpose, enabling a flexible, ad-hoc mix of guidance by the code and extension, lift, or transform by both library and IDE. The code would describe and annotate information such that the algebra can present it using something like a tangible values model.

The Dynamic Runtimes Elephant in the Room

I was at Bret's Strange Loop talk and really struggled with it. Last night, I figured out what my issues were and wrote a post about them here. I'm not a PLT wizard (or even amateur) like many of you but I would appreciate this community's thoughts.

For what it's worth, I do agree that Bret is a fine writer and thinker and we would do well to try and focus harder on usability in PLT.

The last paragraph/tl;dr from my post follows:

While philosophically well-formed, Bret seems to miss the fact that runtime support is required for a Visible Editing Experience to emerge. If the industry still doesn't understand that dynamism is about the runtime rather than types, clamoring for a magic editor will get us nowhere. I want to see a more interactive, tangible environment in the future as well but we cannot get there by arguing that IDE/editor writers need to step up their game. We need to make a concerted argument for the resurrection of highly reflective systems in both research and industry. Once systems with robust reflective capabilities are widespread, realizing a vision such as that described will be a week long hack rather than a decade-long million man-hour slog.

You're absolutely right.

The kind of system he's talking about can only be built on a reflective, interactive runtime. I don't think he has a problem with that.

It's entirely possible to build reflective, interactive runtimes for languages that are usually statically compiled. I mean, there are C interpreters for pete's sake. But it definitely raises the bar for language developers, not just IDE developers. Full integration with the runtime means, probably, that the IDE and dynamic runtime can only be produced as additional build targets on the same code base that builds the static compiler.

That makes language development *MUCH* harder because you have to support effectively three products with a tight set of interdependencies, one of which is highly UI intensive and two of which are fiendishly complex and have to be bug-for-bug compatible even while one supports a much richer interface.

Social Aspects of Learning and Computation

Where Bret Victor missed out are the concerns he didn't name: programs as social constructs, cooperative learning with other humans, the whole social aspect of computation. His examples, and his vision, seem designed for a single user, living in a social void. Poor, lonely programmer.

If you have your compiler

If you have your compiler you are never lonely.

Great Comment

what a pity LTU doesn't have a "like" button.

Forcing people to write text

Forcing people to write text comments helps keep up the intellectual level.

I thought he was pretty obviously text-blind.

He has some really goood ideas about learnability, but several times some kind of personal cognitive challenge seems to prevent him from being able to make sound arguments for them.

Maybe this is just me, but I kept having "wait, what?" moments when Mr. Victor presented the same information in text and graphics and called the text presentation "without context" and the graphic presentation "in context."

'Cause as far as I can tell that's a complete mistake. The amount of context -- which is to say, related information, presentation in alternate modes, etc -- was identical in both cases. In fact, in his example of a drive around his neighborhood, the graphic presentation is MISSING crucial information. You might get a better idea where you are going in terms of local geography (say, relative to the water), but you will not be able to get there without knowing the names of the streets where you need to turn!

For him to imply that the information in the picture was more usable, indicates some kind of severe text-blindness or lack of confidence in text presentation. He feels that his ability to actually read for comprehension is somehow drastically inferior to some magical thing that happens in his brain when he looks at pictures. Or, more likely, he's psychologically reassured by pictures and they make him feel "safe" enough that he fails to notice when crucial information is missing from them.

So on the one hand I was looking at his points and examples and seeing important techniques to develop usability -- traces of flow-of-control, a view of code where argument names are visible, the classic time-traveling reversible debugger, deep documentation presented in a responsive or exploratory way, understanding how kids learn to use Logo and why, etc.

But on the other hand his arguments in support of these things seemed to me to be often deeply flawed by his apparent text-blindness. He was attacking weak points that the things he was attacking, in some cases, actually don't have, and presenting 'solutions' that, in some cases, were in a different learning mode but not actually more effective.

That said, engaging people in *more* sensory and cognitive modes is always good, especially if they get to pick and choose which mode works best for them. I would seriously hesitate, however, to flatly replace some of the things Victor hates with some of the things he loves. I think doing so would only change whose cognitive style is left out while not changing the fact that people are getting left out. And if done carelessly, it would risk failing to present crucial information (like the street names) in any form.

Ray

it is not "text" = "without

it is not "text" = "without context" and "picture" = "with context".
"text only" = information without context.
"text + picture (both)" = information with context.

I agree with you.

You're absolutely right. Of course "context" means multiple ways of accessing the information - text *and* something else forms a context, just as pictures *and* something else form a context.

And that is, in fact, the essence of my problem with this whole essay. Mr Victor apparently does not know that. He's pretty explicit about having one label per presentation, but one presentation, of whatever type, does not make a context.

There is no evidence at all that he considers the text to be any part of what he called "in context" nor the pictures/animations/etc to be any part of what he called "without context." His presentation of the two is completely symmetric as regards labels.

Ray

Light Table

I read this and thought, "Hey I wonder if Bret and that Light Table guy ever met."

Turns out Chris Granger (Light Table) refers to Bret Victor quite often.

And I wonder if Bret Victor

And I wonder if Bret Victor was inspired by Jonathan Edward's subtext work. None of us work in a vacuum, please.