1. 3

    There was a recent discussion about whether file-based APIs were better or worse the specific ones (https://lobste.rs/s/ckqzbn/why_filesystems_have_loose_coupling_your).

    I think a lot of the pros and cons of that API choice are illuminated by unveil.

    Things can always be implemented either way (layered on top of the generic mechanism, or given their own specific mechanism). But if you then enhance the general mechanism with a new feature, you get to use that for all of the functionality you have layered on top.

    (As a side thought - it’s kind of a nice thought experiment: If the only syscalls were open/close/read/write and friends - how would you (a) provide an interface for all other system calls and (b) put them together in a filepath hierarchy so that unveil() grouped things nicely.)

    1. 4

      I know very little about either of these topics, but I wonder if it would be interesting to combine OpenBSD’s security syscalls like pledge and unveil (is there a generic name for these? Privilege-based security?) with Plan 9’s extreme everything is a filesystem approach.

      1. 5

        The amount of syscalls in Plan9 is so small that pledge(2) wouldn’t make much sense. And unveil(2) wouldn’t really be necessary as there are already mount namespaces, a process can unmount paths and then use rfork(2)s RFNOMNT flag to disallow mounting new resources.

        https://swtch.com/~rsc/talks/nauth.pdf

        1. 3

          In other words, Plan 9 obviated the need for these approaches. It really was the wave of the future, but it hasn’t reached the shore yet. Yet …

    1. 5

      For the ‘transfer repo’ use case, you can just tgz the repo (which will include the .git dir and thus the whole history).

      I didn’t know you could use a bundle to package changesets though - that’s really useful.

      1. 1

        There used to be a bunch of explosions if you did that to a repository with submodules, iirc; have those been fixed?

        1. 2

          Sorry, I don’t know. Can you recall anything more about the problem case?

          Tarring up a dir in one place and untarring it somewhere else is functionally just a copy (or move if you don’t use the old one) i.e. it’s the same as ‘mv repo new_repo’.

          I can’t see how that can break unless it is related to the per-user settings? (e.g. someone using ssh based remotes and the new user not having ssh creds).

          So yes - I can see how copying the repo as I described doesn’t remove any remotes (including submodules), which could cause some confusion.

          1. 2

            Should have done some research on my own before I asked, sorry! Looks like the last time this bit me I was really unlucky; it’s a problem that only affected two minor versions of git.

      1. 2

        The “who is client and who is server” question seems unfair.

        There is a controlling level…analogous to the shell, which has the job of wiring up the inputs and outputs.

        Strawman solution for RPC-over-HTTP…..each line-at-a-time service also accepts a destination url, which accepts line-at-a-time input. So every service reads the http op, performs its function, then pushed onto the next. The final service can be the ‘append to file X’ service.

        If we’re mimicking Unix cmdline we can also have the services conspire to ‘batch’ (i.e. block buffer) lines for efficiency.

        RPC may have several problems, but it doesn’t seem this is one of them (it seems no worse then Unix pipeline)

        1. 14

          I don’t buy it because the real protocol is what you read and write from the file, not that you can read and write files. And if the “file” is a directory, what do the filenames you read and write from/to it mean?

          So is there really any difference between open(read("/net/clone")) and net_clone();? The author seems to say the former is more loosely coupled than the latter because the only methods are open and read on the noun that is the file…. but really, you are stating exactly the same thing as the “verb” approach (if anything, I’d argue it is more loosely typed than loosely coupled). If a new version wants to add a new operation, what’s the difference between making it a new file that returns some random data you must write code to interpret, and a new method that returns some data you must write code to use?

          1. 24

            So is there really any difference between open(read(”/net/clone”)) and net_clone();

            Yes: The fact that you can write tools that know nothing about the /net protocol, and still do useful things. And the fact that these files live a uniform, customizable namespace. You can use “/net/tcp/clone”, but you can also use “/net.home/tcp/clone”, which may very well be a completely different machine’s network stack. You can bind your own virtual network stack over /net, and have your tests run against it without sending any real network traffic. Or you can write your own network stack that handles roaming and reconnecting transparently, mount it over /net, and leave your programs none the wiser. This can be done without any special support in the kernel, because it’s all just files behind a file server.

            The difference is that there are a huge number of tools you can write that do useful things with /net/clone that know nothing about what gets written to the /net/tcp/* files. And tools that weren’t intended to manipulate /net can still be used with it.

            The way that rcpu (essentially, the Plan 9 equivalent of VNC/remote desktop/ssh) works is built around this. It is implemented as a 90 line shell script It exports devices from your local machine, mounts them remotely, juggles around the namespace a bit, and suddenly, all the programs that do speak the devdraw protocol are drawing to your local screen instead of the remote machine’s devices.

            1. 5

              You argue better than I can, but I’ll add that the shell is a human interactive environment, C api’s are not. Having a layer that is human interactive is neat for debugging and system inspection. Though this is a somewhat weaker argument once you get python binding or some equivalent.

              1. 1

                I was reminded of this equivalent.

              2. 1

                But in OOP you can provide a “FileReader” or “DataProvider”, or just a FilePath that abstracts either where the file is or what you are reading from too. The simplest would be the net_clone function above just taking a char* file_path, but in an OOP language the char* or how we read from whatever the char* is can be abstracted too.

                1. 2

                  Yes, but how do you swap it out from outside your code? The file system interface allows you to effectively do (to use some OOP jargon) dependency injection from outside of your program, without teaching any of your tools about what you’re injecting or how you need to wire it up. It’s all just names in a namespace.

                  1. 0

                    without teaching any of your tools about what you’re injecting or how you need to wire it up

                    LD_PRELOAD, JVM ClassPath…

              3. 6

                So is there really any difference between open(read(”/net/clone”)) and net_clone();?

                Yes, there is. ”/net/clone” is data, while net_clone() is code.

                1. 4

                  I don’t buy it because the real protocol is what you read and write from the file, not that you can read and write files

                  Yes - but the read()/write() layer allows you to do useful things without understanding that higher-level protocol.

                  It’s a similar situation to text-versus-binary file formats. Take some golang code for example. A file ‘foo.go’ has meaning at different levels of abstraction:

                  1. golang code requiring 1.10 compiler or higher (uses shifted index expression https://golang.org/doc/go1.10#language)
                  2. golang code
                  3. utf-8 encoded file
                  4. file

                  You can interact with ‘foo.go’ at any of these levels of abstraction. To compile it, you need to understand (1). To syntax-highlight it you only need (2). To do unicode-aware search and replace, you need only (3). To count the bytes, or move/delete/rename the file you only need (4).

                  The simpler interfaces don’t allow you to do all the things that the richer interfaces do, but having them there is really useful. A user doesn’t need to learn a new tool to rename the file, for example.

                  If you compare that to an IDE, it could perhaps store all the code in a database and expose operations on the code as high-level operations in the UI. This would allow various clever optimisations (e.g. all caller/callee relationships could be maintained and refactoring could be enhanced).

                  However, if the IDE developer failed to support regular expressions in the search and replace, you’re sunk. And if the IDE developer didn’t like command line tools, you’re sunk.

                  (Edit: this isn’t just one example. Similar affordances exist elsewhere. Text-based internet protocols can be debugged with ‘nc’ or ‘telnet’ in a pinch. HTTP proxies can assume that GET is idempotent and various cacheing headers have their standard meanings, without understanding your JSON or XML payload at all.)

                1. 1

                  On the EINTR/retry issue, it’s not clear to me why a simple ‘return to userspace and retry the instruction’ is harder to implement.

                  There is kernel-side state associated with the fd (seek position, network buffers etc) but these need to be maintained anyway for an EINTR return.

                  Put another way - what work can the kernel avoid in the ‘return EINTR and let the userspace application call the system call again’ scenario which is required in the ‘return to userspace at PC-1’?

                  1. 5

                    There are two big issues: changing entry conditions, and blocking after interrupts.

                    Blocking after interrupts is fairly obvious when you think about it. If you are blocking on some syscall in your main thread, receive a signal, and your syscall is restarted automatically, you cannot respond to that signal in your main thread at all. You’d just keep blocking. If the signal was SIGINT and you would want to cleanly shut down, you can’t. You’d be stuck until your syscall unblocks, which could never happen.

                    Changing entry conditions are much more tricky. For example, some syscalls have timeouts—the amount of time they should block before returning regardless of their success or failure. If you start a syscall with a 10 second timeout, what happens if that syscall is cancelled and restarted 5 seconds in? If the same arguments are used, it would be as if the timeout had been 15 seconds. If you receive 1 signal per second indefinitely, the syscall will block forever. Unlikely but possible.

                    When you call something with a 10 second timeout, you actually mean 10 seconds from now. To restart these syscalls, the kernel would need to preserve that start time entry condition. That’s fairly doable, but there are other entry conditions that aren’t doable at all. If you’re writing size-formatted output to the console, and you receive SIGWINCH, you don’t want to proceed with the write. Instead you need to reformat the output to match the new console dimensions. The kernel certainly can’t do that for you.

                    There are so many reasons you might want to change syscall parameters after an interrupt, or do something else entirely. The kernel can’t know all of them. And designing an interface to conveniently accommodate all of them is a lot harder. Thus the worse-is-better solution: never assume what a program wants to do after an interrupt.

                    1. 2

                      Signals, I think, is now fairly well accepted as one of the “ugly” parts of posix.

                      signalfd was an attempt at fixing it…. Here is more discussion of that..

                      https://ldpreload.com/blog/signalfd-is-useless

                      Having spend the last week battling the fine fine fine corner cases of signal handling….

                      Sigh.

                      I wish linux had something better.

                      1. 1

                        Thanks for this.

                        If you are blocking on some syscall in your main thread, receive a signal, and your syscall is restarted automatically, you cannot respond to that signal in your main thread at all. You’d just keep blocking.

                        I’m still not getting this one. I’d envisage:

                        • application calls blocking read()
                        • application receives SIGHUP
                        • kernel sees incoming signal and stops doing read()
                        • kernel calls up into user spaces to run signal handler in user context for SIGHUP. As far as application goes, it’s still doing read(), but signals can happen any time anyway, so no problem here?
                        • kernel restarts read()

                        If you’re writing size-formatted output to the console, and you receive SIGWINCH, you don’t want to proceed with the write.

                        I’m not sure I agree. Given that arriving signals are inherently racy, I think it could be considered to also be correct to re-run the system call without the application making a new choice based on the new information. (The system call could easily have completed before the signal arrived - and the application should be prepared for that eventuality).

                        When you call something with a 10 second timeout, you actually mean 10 seconds from now. To restart these syscalls, the kernel would need to preserve that start time entry condition.

                        This is a good point. However, the optimist in me would like to think this is always solvable with API design. (In the timeout case, this would involve absolute timeout rather than relative).

                        1. 2

                          Your scenario doesn’t work, because signal handlers are extremely restricted. Signal handlers must be reentrant with respect to all other signal handlers, meaning they can’t allocate memory, can’t use most syscalls, can’t use any libc functions that set errno, and can’t non-atomically modify non-local variables.

                          For this reason, signal handlers usually set a global volatile flag and return immediately, allowing the main thread to catch EINTR, check the flag, and handle the signal without restriction.

                          In the SIGWINCH example, doing what you suggest causes significant visual tearing, even though it’s technically correct. But that assumption about racy signals only works if all syscalls are guaranteed to complete.

                          However, the optimist in me would like to think this is always solvable with API design.

                          Perhaps. Until such an API actually exists, we must write interruptible code somehow.

                          (In the timeout case, this would involve absolute timeout rather than relative).

                          What absolute time? The system clock can change. The clock that can’t is the monotonic raw clock, which simply ticks upwards from an unspecified time. I don’t think an API that takes a deadline with respect to a meaningless clock beats an API that requires restarting on interrupt.

                          1. 1

                            Your scenario doesn’t work, because signal handlers are extremely restricted.

                            Yes they are, but I don’t think that stops the kernel from invoking them and then restarting the system call afterwards.

                            In the SIGWINCH example, doing what you suggest causes significant visual tearing

                            Which could already occur. We’re just (slightly) increasing the window in which delivery of the signal will cause it.

                            What absolute time?

                            The one with the same semantics as a relative timeout - i.e. monotonic. That is the behaviour you would get if you specified “10 seconds from now”.

                            1. 2

                              Your scenario doesn’t work, because signal handlers are extremely restricted.

                              Yes they are, but I don’t think that stops the kernel from invoking them and then restarting the system call afterwards.

                              What I think you’re missing here is that since you can’t do much in a signal handler besides setting a flag for the main thread to check, you must have a way to interrupt/cancel/unblock whatever system call is blocking the main thread so that the main thread can start doing whatever the signal called for. If the kernel automatically restarts the system call, the main thread obviously can’t go check that flag and react accordingly.

                              1. 2

                                Yes, you’re right, thank you. But in that case, the interrupted return is the feature, not the bug?

                                The whole ‘worse is better’ thing is cast as “return EINTR” being an undesirable property.

                                1. 2

                                  Yes indeed, it is a feature. But it is a feature that complicates life for every call that doesn’t care and doesn’t want to be interrupted.

                                  1. 2

                                    OK, but it isn’t anything to do with Worse is Better, right? The article characterises it as a deficiency of implementation.

                                    In fact, it’s a feature you need if you want to abort a blocking operation.

                                    afaics, the only problem is the default. You probably want the SA_RESTART behaviour by default (and perhaps that should be per-fd, rather than per-signal).

                                    But I don’t think the characterisation in the article is fair, unless I’ve missed something.

                                    1. 1

                                      Worse is better means choosing a design that’s worse in some cases because at least it works for all cases. EINTR absolutely embodies that description. It’s a dead simple way to make sure you can always handle interrupts, but extremely tedious to work with in the common case.

                                      1. 2

                                        That was my understanding of the pt made in the article and the received wisdom regarding this article.

                                        However, I thought the conclusion of the discussion above was that it wasn’t any easier than not handling the interrupt. In fact, the interrupted behaviour is required (it is a feature) in many cases.

                                        I still fail to see how it is easier for the kernel to return EINTR rather than restart the syscall. (Apart from the API issue mentioned regarding entry conditions, e.g. relative/absolute times).

                                        It might help if someone can outline the desired behaviour. It isn’t “restart syscall”, since that has been disposed of as undesirable above.

                                        I think my point is: “this article says the Unix EINTR approach is a short cut in kernel implementation which has imposed a cost on userspace since then”. However:

                                        a) I can’t see the shortcut being taken (no one has pointed out how it is easier to restart a read() than return EINTR) b) arguments above are for EINTR being a good thing

                                        Is there a 3rd way (other than ‘abort read() early and return EINTR’ or ‘restart read()’) which I’m missing here?

                                        1. 2

                                          I’m totally out of my element here, but did not EINTR evolve over years? Like, it started really cheap and not useful, but it became better (and more complex) over time? Is this what the author referred to when they said that unix/c improved from 50% to 90%?

                        2. 0

                          Thus the worse-is-better solution: never assume what a program wants to do after an interrupt.

                          One might argue that interrupts are a broken IPC system. Errno definitely is broken.

                          However syscalls can fail (as any other computation).

                      1. 2

                        Don’t all VCSs have tools to modify history? I think svnadmin does: http://oliverguenther.de/2016/01/rewriting-subversion-history/ (assuming there aren’t any blockchain-based VCSs. I daren’t look)

                        If the distinction being drawn is ‘admin’ vs ‘user’ tooling, I guess - like workflow - git punts that to the surrounding culture and environment (as it does “which version is the ‘master’” - which is the same feature/bug of any DVCS).

                        I admit I like being able to say “v234” but really, what that means is “v234 of the (single) upstream repo which can change any time the upstream repo manager runs svnadmin”.

                        There’s nothing to stop github putting a sequential “v1, v2, v3, …” on commits to master or otherwise blessing some workflow.

                        I think the differences aren’t so much about features + capability and tooling as culture.

                        1. 2

                          git is a merkle-tree-based system, which is what I assume you meant by “blockchain-based” in this context

                          1. 1

                            Yes it is, but no - that’s not what I meant. I mean that I expect every VCS to be able to rewrite history since the data files are under control of the admin. git can do it, svn can do it. You can edit RCS files by hand if you want to (unsure if there is tooling to do it).

                            i.e. linus can rewrite his git history. It will be out of sync with other people, but that is then a social issue, not a technical one (I admit this is a fine point).

                            The only time you can’t rewrite history is in the “public immutable” world of blockchain - since the data files aren’t under your control. I don’t know if someone has built a vcs like that and my comment was really just a side swipe at blockchain hype.

                            1. 1

                              you can if you get 51%

                              1. 1

                                https://github.com/clehner/git-ssb not exactly blockchain, but immutable history just the same.

                          1. 3

                            This problem (test assertion not running) is a general problem in most testing frameworks which require you to call a ‘fail’ (directly or indirectly) as the only way to signal a problem. Sometimes you fail to call fail.

                            Perl produced a test system called TAP (Test Anything Protocol) - now ported to other environments: https://testanything.org/

                            This uses an approach where:

                            • both positive (ok) and negative (not ok) assertions are reported
                            • the test begins with a statement of the total number of assertions expected (you can finish with this too)
                            • a test whose number of assertions doesn’t match the plan (too many or too few) has failed

                            There is a small piece of extra work (when you add a test, you have to increase the planned number), but this is minor and the approach guards against tests failing to run for any reason (if your test fails so drastically that it can’t even emit a plan, the test harness will/should report that).

                            (Here’s one perl API to emit TAP: http://perldoc.perl.org/Test/More.html)

                            1. 1

                              Interesting. Yeah, I’ve experienced this when testing asynchronous code. Jest, for example, lets you set your expected number of assertions up front, per test, which is nice.

                              I guess I just wasn’t expecting this to come up during a totally synchronous test. And obvious that was a dangerous assumption to make.

                              Thanks for the links!

                            1. 12

                              I think I’ve read this paper a half dozen times now after seeing someone or other wax lyrical about it online. And I just don’t get why people like it so much. Which means either I’m too much of an insider and this is old news, or I don’t appreciate what I don’t know.

                              Part of the problem is that it seems to overstate its contributions. It isn’t really “identifying” causes of complexity. Fred Brooks pointed out essential vs accidental complexity back in 1975, and there’s been a steady stream of articulation ever since. To any Haskeller the point that complexity comes from state is old news. Relational algebra is ancient. At best OP is laying things out in a new way.


                              This time I figured I’d speed up my rereading time by instead chasing down past threads. And I discovered a decent summary. This was helpful, because it helped me to focus on the ‘causes of complexity’ portion of OP.

                              Causes of complexity according to OP:

                              • State (difficulty of enumerating states)
                              • Control (difficulty of choosing an ordering)
                              • Concurrency
                              • Volume
                              • Duplicated/dead code, unnecessary abstraction

                              Compare the list I made a couple of years ago:

                              • Compatibility. Forcing ourselves to continue supporting bad ideas.
                              • Vestigial features. Continuing to support stuff long after it’s been obsoleted by circumstances. Simply because it’s hard to detect obsolescence.
                              • People churn. Losing institutional fingerspitzengefühl about the most elegant place to make a given change. Or knowing when some kludge has become obsolete.

                              Comparing these two lists, it looks like there’s a tension between the top-down and bottom-up views of software management. In the bottom-up view people seem to think about software like physics, trying to gain insight about a system by studying the atoms and forces between atoms. You tend to divide complexity into state and order, essential and accidental. Reductionism is the occupational hazard.

                              In my top-down view I tend to focus on the life cycle of software. The fact that software gets more complex over time, in a very tangible way. If we could avoid monotonically adding complexity over time, life would be much better. Regardless of how many zeroes the state count has. In this worldview, I tend to focus on the stream of changes flowing into a codebase over time, alongside the stream of changes happening to its environment. This view naturally leads me to categorize complexity based on its source. Is it coming from new feature requirements, or changes to the operating environment? How can I keep my raft slender as I skim the phase transition boundary between streams?

                              The blind spot of the bottom-up view is that it tends to end up at unrealistic idealizations (spherical cows as @minimax put it in this thread). The blind spot of the top-down view is that there’s a tendency to under-estimate the complexity of even small systems. Blub. The meme of the dog saying “this is fine” while surrounded by flames.

                              It seems worth keeping both sides in mind. In my experience the top-down perspective doesn’t get articulated as often, and remains under-appreciated.

                              1. 5

                                Here’s my take on it: https://news.ycombinator.com/item?id=15776629

                                I also don’t think it’s a great paper. It’s long on ideas but short on evidence, experience, and examples. I don’t think you’re missing anything.

                                1. 4

                                  I have a similar take to you. I think it’s one of those papers that is easy to get excited about and everyone can agree that complexity is bad and all that. But I have not seen any successful application of the ideas in there. The author’s haven’t even successfully implemented the ideas beyond a little prototype, so we don’t have any idea if what they say actually pans out.

                                  And to toss my unscientific hat into the ring: IME the biggest source of complexity is not programming model but people just not being disciplined about how they implement things. For example, I’m currently working in a code base where the same thing is implemented 3 times, each differently, for no obvious reason. On top of that, the same thing is some times and id, sometimes the id is a string and sometimes an int, and sometimes the string is a URL, and it’s never clear when or why. This paper is not going to help with that.

                                  1. 2

                                    If what you say is true, then the success of LAMP stacks with associated ecosystems for new people and veterans alike might make it high on “evidence, experience, and examples.” That architecture worked for all kinds of situations even with time and money limitations. Except that the specific implementation introduces lots of the complexities they’d want people to avoid. So, maybe instead the Haskell answer to a LAMP-style stack or something like that fitting their complexity-reduction ideas.

                                    Although the others shot it down as unrealistic, your characterization seems to open doors for ways to prove or refute their ideas with mainstream stuff done in a new way. Maybe what they themselves should’ve have done or do they/others do later.

                                    1. 4

                                      Yes, so depending on the scope of their claims, it’s either trivial and doesn’t acknowledge the state of the art, or it’s making claims without evidence.

                                      Appreciating LAMP is perhaps nontrivial. Google services traditionally used “NoSQL” for reasons of scaling, but the relatively recent development of Spanner makes your architecture look more like LAMP.

                                      But either way I don’t think that LAMP can be “proven” or “refuted” using their methodology. It’s too far removed from practice.

                                  2. 4

                                    In my top-down view I tend to focus on the life cycle of software. The fact that software gets more complex over time, in a very tangible way. If we could avoid monotonically adding complexity over time, life would be much better.

                                    Thanks for the interesting commentary. Some parts definitely resonated, particularly about the half-features and difficulty of knowing how and where to make the right change.

                                    This is only the germ of an idea, but it is perhaps novel and perhaps there is an approach by analogy with forest management. Periodic and sufficiently frequent fires keep the brush under control but don’t get so big that they threaten the major trees or cause other problems.

                                    Could there be a way of developing software where we don’t look at what is there and try to simplify/remove/refactor, but instead periodically open up an empty new repo and move into it the stuff we want from our working system in order to build a replacement? The big ‘trees’ of well understood core functionality are most easily moved and survive the burn, but the old crufty coupling doesn’t make the cut.

                                    Some gating on what would be moved would be needed. The general idea though is that only sufficiently-well-understood code would make it across to the new repo. And perhaps sufficiently reliable/congealed black boxes. It would interplay a lot with the particularly language’s module/package and testing systems.

                                    The cost would be periodic re-development of some features (with associated time costs and instability). The benefit would be the loss of those code areas which accrete complexity.

                                    1. 2

                                      Yes, definitely worth trying. The caveat is that it may be easy to fall into the trap of a full rewrite. There’s a lot of wisdom encoded in the dictum to avoid rewrites. So the question becomes: how would you make sure you don’t leave behind the most time consuming bugfixes you had to make in production on the old repo? Those one-liners that took a week to write?

                                    2. 3

                                      This paper was written in 2006, two years before Applicatives were introduced. The Haskell community’s understanding of how to best structure programs has been refined a lot in that time and I think you underestimate the insights of this paper even if it is only a refinement of Brooke’s ideas from 40 years ago.

                                      1. 1

                                        Thanks, I hadn’t seen that paper. What made you cite it in particular?

                                        1. 1

                                          It’s where Applicatives were introduced, as far as I know.

                                          1. 7

                                            Can you describe the connection you’re making between Applicatives and this paper?

                                            1. 1

                                              I got the impression that akkartik was saying that OOTP hadn’t added much new. My claim is that the fact that Applicatives were only introduced 10 years ago shows that the bar for novelty is actually quite low.

                                      2. 1

                                        “This was helpful, because it helped me to focus on the ‘causes of complexity’ portion of OP.”

                                        That’s the part I read in detail. I skimmed the second half saving it for later since it was big. The first half I liked because it presented all those concepts you listed in one location in an approachable way. It seems like I learned that stuff in pieces from different sub-fields, paradigms, levels of formality, etc. Seeing it all in one paper published ahead of some of these things going into mainstream languages was impressive. Might have utility to introduce new programmers to these fundamental concepts if nothing else I thought. Understanding it doesn’t require a functional programming or math back ground.

                                        Ok, so that’s their requirements. Minimax mentions things like business factors. You mention changes with their motivations. I’ll add social factors which includes things that are arbitrary and random. I don’t think these necessarily refute the idea of where the technical complexity comes from. It might refute their solutions for use in the real world such as business. However, each piece of the first half is already getting adoption in better forms in the business world on a small scale. There’s also always people doing their own thing in greenfield projects trying unconventional methods. So, there’s at least two ways their thinking might be useful in some way. From there, I’d have to look at the specifics which I haven’t gotten to.

                                        I do thank you for the Reddit search given those are about the only discussions I’m seeing on this outside here. dylund’s comments on Forth were more interesting than this work. Seemed like they were overselling the positives while downplaying negatives, too, though.

                                      1. 2

                                        Huh, what would a federated message board look like? I guess I could see a reddit-like one where each sub could be on a different server, but you’d have a shared account around them all. Still one server per forum, so you can have consistent ordering of stories and comments. I’m not really sure what the benefit is to anyone of having a shared account among a ton of federated board servers, though. It just preserves reddit weirdness like sharing massively different karma amounts between joke boards and deep research boards.

                                        Lobsters is meant to have one main page though. How would you do consistent ordering of the front page if stories were federated?

                                        1. 4

                                          what would a federated message board look like?

                                          Usenet, I think. Threaded messages (with different people getting a different, but eventually consistent view of the thread). Each lobste.rs post would be a new top-level thread.

                                          You’d lose voting and ranking on a straight usenet model, but that would be a small extension (usenet already supports control messages - you’d just have upvotes/downvotes propagated as a type of control message and your ‘top level’ view respecting the votes and an aging algorithm etc.

                                          1. 1

                                            I’m not at all convinced that you need that consistent view. Twitter doesn’t have one - everyone sees their own slice of things that they’re paying attention to.

                                            Having some consistency is a prerequisite for a place to be a community, though, so it would certainly be a very different form of interaction.

                                          1. 3

                                            The offhand ‘even perl’ in there struck me as unfair. It reminds me that perl is actually pretty fast (specifically at startup, but my recollection was also that it runs quickly):

                                            $ time for i in `seq 1 1000`; do perl < /dev/null; done
                                            
                                            real    0m2.786s
                                            user    0m1.337s
                                            sys     0m0.686s
                                            
                                            $ time for i in `seq 1 1000`; do python < /dev/null; done
                                            
                                            real    0m19.245s
                                            user    0m9.329s
                                            sys     0m4.860s
                                            
                                            $ time for i in `seq 1 1000`; do python3 < /dev/null; done
                                            
                                            real    0m48.840s
                                            user    0m30.672s
                                            sys     0m7.130s
                                            
                                            
                                            1. 1

                                              I can’t comment on how fast Perl is, but you are measuring the time taken to tear down here too.

                                              The correct way would be to take the raw monotonic time immediately before invoking the VM, then inside the guest language immediately print it again and take the difference.

                                              P.S. Wow Python3 is slower.

                                              1. 2

                                                but you are measuring the time taken to tear down here too.

                                                I guess so? I’m not sure that’s a useful distinction.

                                                The people wanting “faster startup” are also wanting “fast teardown”, because otherwise you’re running in some kind of daemon-mode and both times are moot.

                                                1. 1

                                                  The people wanting “faster startup” are also wanting “fast teardown”

                                                  Yeah, I guess I agree that they should both be fast, but if we were measuring for real, I’d measure them separately.

                                                  1. 1

                                                    I’m not sure that’s a useful distinction.

                                                    If latency matters then it could be. If you’re spawning a process to handle network requests for example then the startup time affects latency but the teardown time doesn’t, unless the load gets too high.

                                                2. 1

                                                  Hah before I read the comments I did the same thing! My results on a 2015 MBP - with only startup and teardown on an empty script, and I included node and ruby also:

                                                  ~/temp:$ time python2 empty.txt 
                                                  real    0m0.028s
                                                  user    0m0.016s
                                                  sys     0m0.008s
                                                  
                                                  ~/temp:$ time python3 empty.txt 
                                                  real    0m0.042s
                                                  user    0m0.030s
                                                  sys     0m0.009s
                                                  
                                                  ~/temp:$ time node empty.txt 
                                                  real    0m0.079s
                                                  user    0m0.059s
                                                  sys     0m0.018s
                                                  
                                                  ~/temp:$ time perl empty.txt 
                                                  real    0m0.011s
                                                  user    0m0.004s
                                                  sys     0m0.002s
                                                  
                                                  ~/temp:$ time ruby empty.txt 
                                                  real    0m0.096s
                                                  user    0m0.027s
                                                  sys     0m0.044s
                                                  
                                                  1. 2

                                                    Ruby can do a bit better if you don’t need gems (and it’s Python 3 here):

                                                    $ time for i in $(seq 1 1000); do ruby </dev/null; done
                                                    
                                                    real	0m31.612s
                                                    user	0m27.910s
                                                    sys	0m3.622s
                                                    
                                                    $ time for i in $(seq 1 1000); do ruby --disable-gems </dev/null; done
                                                    
                                                    real	0m4.117s
                                                    user	0m2.848s
                                                    sys	0m1.271s
                                                    
                                                    $ time for i in $(seq 1 1000); do perl </dev/null; done
                                                    
                                                    real	0m1.225s
                                                    user	0m0.920s
                                                    sys	0m0.294s
                                                    
                                                    $ time for i in $(seq 1 1000); do python </dev/null; done
                                                    
                                                    real	0m13.216s
                                                    user	0m10.916s
                                                    sys	0m2.275s
                                                    
                                                    1. 1

                                                      So as long python3 is faster than ruby/node, we are ok…?

                                                  1. 4

                                                    Seen your update including perl. You ask:

                                                    Then the next question is: does it make programming easier (i.e. thinking more about programming and less about naming) or harder (i.e. naming things is documentation).

                                                    and the answer to that is that if you’re writing big, maintained perl programs, you typically use explicitly named lexicals. Except where the scope is very short. Not least because the un-named abbreviations have dynamic not lexical scope:

                                                    #!/usr/bin/perl
                                                    use 5.10.0;
                                                    
                                                    for (qw/foo bar baz/) {
                                                      do_stuff();
                                                      say;      # output $_
                                                    }
                                                    
                                                    sub do_stuff() {
                                                      say "wtf $_" if /z/;
                                                    }  
                                                    
                                                    $ perl tt.pl
                                                    foo
                                                    bar
                                                    wtf baz
                                                    baz
                                                    
                                                    
                                                    1. 12

                                                      It’s not fashionable, but perl took from Larry Wall’s linguist background the concept of ‘it’ - i.e. the “thing we are talking about”.

                                                      It’s spelt “$_” explicitly in perl, but also many operations use $_ implicitly if no arg is given (e.g. regex/sub, print, etc). Also the looping constructs (for/map/grep) bind $_ to the ‘current element’. So you can:

                                                      print for @lines;
                                                      

                                                      and have:

                                                      • the for loop loop over each element in the array/list
                                                      • bind ‘$_’ as a (mutable!) alias to each list element
                                                      • invoke print (which defaults to $_ in the absence of other args)

                                                      Or:

                                                      while (<STDIN>) {   # Loop over each line on input
                                                        chomp;            # remove trailing \n
                                                        s/foo/bar/;       # regex-and-replace
                                                        say;              # Print to stdout with \n
                                                      }
                                                      

                                                      The schwartzian transform (https://en.wikipedia.org/wiki/Schwartzian_transform) uses this, and also the convention/feature that in a block/lambda passed to ‘sort’, the two items under comparison are ‘$a’ and ‘$b’. Which are hence sufficiently magic that you should never use them for any other purpose in perl :-)

                                                      1. 4

                                                        There’s also a great section on anaphoric macros in On Lisp.

                                                        1. 2

                                                          anaphoric macros create a name that must be referenced to actually refer to it though. I guess in Perl, it’d be roughly equivalent to super lisp pseudocode:

                                                          (defparameter *it* (make-parameter #f))
                                                          
                                                          ;; str defaults to *it* if called without arg
                                                          (define (chomp (str *it*))
                                                             ....)
                                                          
                                                          (define (say (str *it*))
                                                               ...)
                                                          
                                                          (let loop ((*it* (get-line)))
                                                              (if *it*
                                                                 (begin
                                                                    (set *it* (chomp))
                                                                    (set *it* (sub ...))
                                                                    (say)
                                                                    ...)))
                                                          
                                                        2. 2

                                                          The jQuery library for JavaScript also supports a similar feature. In a function passed to $.each, this will be the current array element, and in an event handler, this will be the element that the event was fired on (which I think matches the browser’s DOM event handlers). The handlers can also take those same variables as optional function parameters if you want to name them.

                                                        1. 2

                                                          Is the birthday paradox correct in this case? We’re not looking for any two coins the same colour, we’re looking for a coin the same colour as the one we already have?

                                                          1. 2

                                                            I think you’re right. The current metaphor describes a second preimage attack. I’ll update the post. Thank you.

                                                          1. 2

                                                            Does that kill the idea? Perhaps I’ve misunderstood, but is it saveable with the notion that the system can know:

                                                            • the time a config file was written/last read
                                                            • the version of the language on the system in use at that time
                                                            • language constructs which have changed since then

                                                            Think about something like ‘go fix’ which programmatically move you from one version to another.

                                                            Either at language upgrade time, or at config file load time, you could run language upgrade steps and checks.

                                                            1. 1

                                                              The problem isn’t changing all programs, it’s thinking about the security ramifications of all tools, and the programs they could receive, that may not actually be in the system at the moment.

                                                              I do appreciate the attempt :)

                                                            1. 5

                                                              Thanks for showing this.

                                                              I had fun recently writing my first CPU emulator (for the Z80) in go, which morphed into an assembler, disassembler and ultimately a simple ZX spectrum emulator. https://github.com/jbert/zog

                                                              Got as far as a few key games running well. It is the most fun I’ve had with a compiler since the cryptopals challenges.

                                                              To which I should probably return since there are more and I should have a go at learning some rust.

                                                              1. 7

                                                                I think there’s a lesson here, but mining nuggets out of random email posts isn’t the best delivery mechanism.

                                                                1. 1

                                                                  My attempt at interpreting the lession/question is: “discuss the pros and the cons of ‘fixing’ a compiler warning with a change to the code which is otherwise a no-op”.

                                                                  My take would be: yes, it is worth doing, since it allows you to focus on (and even error the build on) all warnings if you fix those which occur. Fixing in this sense is really just marking the code to say “this is fine, no need to warn on this” (and a comment as to why (along with the cast) would be a good idea).

                                                                  The cost is uglier code (cast and a comment), but if you don’t do this, the warnings which you might care about (= instead of == in an if() ?) will be harder to find, which I think is more important.

                                                                1. 5

                                                                  I question the assertion that the interpreting slowdown is exponential; whether it takes a few dozen cycles per statement (as in a bytecode interpreter) or a few thousand (as in a pure AST interpreter), the same number of statements would need to be executed, and so the slowdown is roughly linear no matter how deep the call stack gets. The exception to that would be if the interpreter were itself getting interpreted for a subroutine call, and I can’t see any reason that an implementation would do such a thing.

                                                                  That said, one interesting place you might look for purely interpreted applications is in the Forth family; it’s fairly common to implement forth using some form of threaded code, which is effectively an AST modulo the fact that neither the S nor the T really apply to forth. You don’t see many programs written in forth directly (though apparently Chuck Moore has written a VLSI layout package), but postscript is fairly common and every implementation I’ve seen has been a pure interpreter.

                                                                  Another possibility, also outside the realm of traditional languages, is TeX, which seems to be completely uncompileable (even to bytecode); I’d definitely consider LaTeX to be a large application written entirely in TeX.

                                                                  1. 2

                                                                    and so the slowdown is roughly linear no matter how deep the call stack gets.

                                                                    You’re the first one to say anything about this! I need to think about this again. Right now I think you’re right. Hm, so the slowdown isn’t from where I think it is in that case. I hope I didn’t just waste more than a month on a false premise.

                                                                    Also, then I’m surprised there aren’t more examples of direct AST interpretation if its “just” a constant factor.

                                                                    Edit: Ok, so I think the issue is that the slowdown scales exponentially with the height of the call stack but that’s unaffected by whether bytecode or direct AST is interpreted. I’m trying to push primitives (like loops) out of the host language and have them written in L (a bit like Forth but its more interpreted than compiled). Now instead of calling a primitive D directly, I call A which calls B which calls C which calls D and each of those have ~10 instructions in L then I get a 1000x slowdown. Is this reasoning right?

                                                                    The exception to that would be if the interpreter were itself getting interpreted for a subroutine call

                                                                    There are many nested layers but the interpreter itself isn’t interpreted (except possibly as a (very deliberate) test).

                                                                    Forth

                                                                    I guess I’m thinking of Forth as basically already bytecode since its a stack machine like most bytecode’s VM. The same goes for Postscript although I don’t know the language.

                                                                    TeX and LaTeX

                                                                    That’s an interesting example. I don’t know how it works internally, just heard it described as a macro expansion language. I’m not sure I’d say that’s interpreted though but its definitely not compiled to either assembly or bytecode. So it would be an example I’m looking for, although I don’t think I can actually make use of this example.

                                                                    1. 4

                                                                      I think the way to look at is is that, as you push the veil back and write more and more of your logic in L, the slowdown applies to more and more of your execution time. It’s still not exponential; if a mixed program takes time t to run, and you have an interpretation overhead of o, the slowest you can possibly get by moving everything to interpretation is t*o and the fastest it can get is t/o. Those bounds could be tightened somewhat by considering the time that it spends in interpreted code vs. compiled code separately; take i to be the ratio of time spent in interpreted code. Then the worst case runtime would be t*i+(1-i)*t*o and the best case runtime would be t*(1-i)+i*t/o.

                                                                      These bounds are difficult to think about intuitively though, because they look at if you moved everything to compiled code or interpreted code. So, instead of looking at the possible performance improvement or loss, let’s look at the actual performance relative to the best case of everything being compiled. If the proportion of interpreted code in your program is i, the slowdown relative to the best possible case will simply be i*o+(1-i)

                                                                      It’s worth noting, though, that for the first few levels of pushing logic into your language, i grows (roughly) exponentially larger. The base has nothing to do with the interpretation overhead though; it’s simply the branching factor of your routines.

                                                                      1. 1

                                                                        Thanks! Your comments here should be all the way at the top!

                                                                        If the proportion of interpreted code in your program is i, the slowdown relative to the best possible case will simply be i*o+(1-i)

                                                                        Yes, I agree and the all-compiled program is a better baseline. This means that I should get a factor of o at worst, even if its all AST interpreted.

                                                                        It’s worth noting, though, that for the first few levels of pushing logic into your language, i grows (roughly) exponentially larger.

                                                                        Hm, I’m wondering if this is what I’ve been seeing and let me to the incorrect exponential conclusion.

                                                                        Still, that means both direct AST interpretation and layering will result in “only” a constant factor slowdown? And its also “only” a constant factor to go from bytecode to native assembly.

                                                                        In general, is there somewhere I can run my reasoning (like this one) by some people for flaws? Other than to posting on lobste.rs that is. I’ve told some others and no-one had seen the problem. At the same time I’m re-doing the time bound estimates, I’m also estimating how much time I lost because of this :(.

                                                                        Ok, one more thing. If functions in level j only calls functions in level j-1 (and level 0 is primitives of the host language) then the time for a level j call is exponential in j, right? (And this happens regardless of whether the levels are (all) interpreted bytecode or direct AST.)

                                                                        And that means I should make calls in as low level as possible (instead of j-1) to make things faster. Except this goes in the wrong direction in terms of the abstraction I’m trying to provide (and potential polymorphism that would allow). For example, not using the object system would be faster than using it.

                                                                      2. 1

                                                                        Is this reasoning right?

                                                                        I don’t think so.

                                                                        Perhaps think of it like this. A function can have more than one call site. Those call sites may have different stack depths. When executing that function, the interpreter will do the same work irrespective of what is on the stack.

                                                                        You may be thinking of nested loops? But that isn’t anything to do with how the program is run, but rather the algorithm which is implemented.

                                                                        1. 1

                                                                          Also, then I’m surprised there aren’t more examples of direct AST interpretation if its “just” a constant factor.

                                                                          You need to take into consideration the number of toy languages that make it to languages in production use, where that constant factor results in a significant savings in time and money. (Less servers, less developer downtime while tests run, etc)

                                                                          1. 1

                                                                            I guess I’m thinking of Forth as basically already bytecode since its a stack machine like most bytecode’s VM. The same goes for Postscript although I don’t know the language.

                                                                            Hmm. Forths are typically pretty low level, and often run directly on hardware (though they often will use their own calling conventions, instead of the C one). The dynamism comes from the fact that words are “compiled” into a list of addresses to the other words, which can basically then be “jumped” to in succession. So, it’s actually slightly strange, and not really a VM with “a list of known operators that you switch on” in the traditional sense.

                                                                            1. 2

                                                                              Good point. Although it means Forths are even more compiled (and a less good example of what I’m looking for) than languages that only stop at the bytecode.

                                                                        1. 1

                                                                          Is it:

                                                                          • operations in a program are implicitly sequenced by their syntactic order (execute this line, then the next)
                                                                          • it is not obvious whether that ordering is logically necessary (e.g. due to a data dependency) or not (this just the order I expressed the list of things which need to happen)
                                                                          • concurrency is the explicit relaxation of that implicit syntactic ordering, leaving only the true dependencies (go foo(); go bar(ch); go baz(ch))

                                                                          The benefits of this are increased clarity and possibly modularity, due to explicit expression of the real dependencies.

                                                                          Parallelism is something you can do to take additional advantage of the concurrency. You can’t just take a normal program and execute every statement simultaneously, since you can run into the implicit dependencies. But you can take a program marked up for concurrency and execute the independent parts of it in parallel.

                                                                          1. 3

                                                                            Something I think important in this kind of discussion is a bit more thought to what to do if your preconditions fail.

                                                                            I’ve seen code in the past which seems to have evolved along the lines of:

                                                                            • use this arg passed in
                                                                            • have a crash report, arg was null
                                                                            • wrap function body in “if (arg != null) { … }” (with no ‘else’)
                                                                            • crash fixed, yay - commit and move on

                                                                            Now rearranging that as a guard clause gets rid of one annoyance (code indent etc) but if the action taken if the guard clause fails is just “return”, we’ve missed the opportunity to fix something.

                                                                            If some of the args don’t meet some criteria, that might be more reasonably an exception or panic() - it could well be a logic error in some other part of the program. Some times it makes sense for a function to be a no-op, but it’s pretty rare (the ‘draft timer’ in the article).

                                                                            1. 1

                                                                              When a failed guard condition raises an immediate failure, that’s called a contract precondition. The general rule I’m familiar with is “return and clean up if the guard might be false, throw an error if the guard shouldn’t be false.”

                                                                              1. 1

                                                                                You’re right, in most cases you’ll need to throw in a guard clause instead of return. You only return when failed preconditions means you don’t need to do any actions, not can’t do. Like deleting a resource when it doesn’t exist.

                                                                              1. 6

                                                                                This is a pretty nice justification for the whole conventional belief system that leads to “abstraction”, “coupling vs cohesion”, “SOLID”, etc. But that whole memeplex is dangerously misguided. It has a kernel of truth, but it’s incomplete, and it’s dangerous because the vast majority of programmers seem to get brainwashed by it until they’re blind to anything else.


                                                                                To see how it’s misleading, let’s focus on this sentence:

                                                                                ..a triangle is actually very highly interconnected for the number of nodes it has..

                                                                                This is a telling sentence, because it highlights that this entire notion of ‘complexity’ is really about density of complexity. Which means that if you just took your triangle and started spliting the vertices into lots of nodes, so that it has lots of nodes along each edge of the triangle, the whole would seem a lot less complex. But you haven’t really made the whole any less complex, you’ve just spread the complexity out into a smooth thin layer. The inherent complexity of the system remains. It’s just harder to find; randomly selected nodes/modules seem simple, and you have to jump through a lot of hoops to find the ‘complex node’ that invariably gets the lion’s share of updates.

                                                                                The whole thing takes me back to my childhood years, when I would shove my stuff into my closet to try to convince my mom I’d cleaned up my room.

                                                                                Abstraction is useful, and it’s useful to think about the right places to draw module boundaries. But as a rule of thumb, mistrust anybody who tries to pontificate about the difference between simplicity and complexity merely by making reference to module/interface boundaries, without any attention to what the system does.


                                                                                So much for tearing down somebody else who in fairness wrote very elegantly indeed. Can I be constructive instead? I’ll suggest an alternative memeplex for thinking about complexity that gets a lot less attention than it should. Complexity comes from the state space of inputs a system needs to handle, and the number of regimes this state space gets broken down into. (Imagine something like the characteristics of a transistor.) The more regimes a state space has, the more intrinsically complex the domain is.

                                                                                If we could demarcate this state space, and make the regimes in the state space explicit in our representation of the system – rather than implicit in lines of code as happens today – I think we’d make far larger strides in controlling complexity than any number of attempts at redrawing internal boundaries between sub-systems. In particular, we’d be able to detect over-engineering and architecture astronomy: they would be situations where a code has significantly higher complexity than the domain it’s trying to address.

                                                                                I think such a representation has to start at (no surprises for people here who’ve heard me ranting before) tests. Tests are great because they’re like a Fourier transform applied to the conventional representation of a program as code. Instead of focusing on what to do at each point in time for a program, looking at a program as a collection of tests tells you how the entire trajectory of processing needs to go for the major regimes in the input space. Tests allow you to get past what your current program does, and think about the essential things any program has to do to achieve the same ends.

                                                                                (I’ve written about this idea before. Hopefully this latest attempt is clearer.)

                                                                                1. 2

                                                                                  I’ll add the state space is my favorite way of looking at complexity because it’s actual values/actions of your software. Different implementations will have different state spaces to assess complexity. Different techniques for reducing complexity can show they do with the state space reductions. Either actual reductions or what it lets us ignore in an analysis for correctness.

                                                                                  1. 2

                                                                                    I like the idea of thinking in terms of a state space! I have a couple of questions:

                                                                                    • Aren’t there really three spaces/sets involved: the set of inputs, the set of possible programs that handle such inputs, and the set of internal program states?
                                                                                    • I noticed you say in the linked post that tests are the only way to delineate boundaries in the state of programs. What about formal verification methods?
                                                                                    1. 1

                                                                                      Sorry I thought I’d responded to this. I’m glad to see more thinking about state spaces of different stripes.

                                                                                      I overstated my case. Formal methods may well be an alternative. I just don’t know much about them.

                                                                                    2. 2

                                                                                      Can you define what you mean by “regime” here?

                                                                                      1. 1

                                                                                        It’s meaning 1b at Merriam -Webster:

                                                                                        a regular pattern of occurrence or action (as of seasonal rainfall)

                                                                                        I see now that the figure of transistor characteristics I linked to above refers to active and saturation “regions”. I seem to recall learning them as regimes in undergrad in India.

                                                                                        Basically it’s a subset of the input space for which the program behavior is the same. How you define “same” is flexible. One definition would be identical control flow paths through the program. Alternatively you can think of timing attacks on encryption schemes as exposing regimes in the underlying algorithm.

                                                                                      2. 1

                                                                                        Whether the triangle has three nodes, or many along each edge, it still has the same number of loops: 1. That’s perhaps a more important observation. A graph that’s easy to color is probably a graph with few loops.

                                                                                        1. 1

                                                                                          Loops might be part of it, but imagine a ‘necklace’ of loops. Each loop can be 2-coloured, and you can join the links at points with the same colour, so you can have as many loops as you like and still only need 2 colours.