HN user

haradion

40 karma
Posts0
Comments30
View on HN
No posts found.

It doesn't really hurt in practice because it's only one part of the full backup procedure. Deduplicate to save space; re-duplicate the (smaller) backups to separate media for redundancy; scrub regularly for bit rot and rotate your media. A proper backup system requires all but the first of those anyway.

All these named reset types could just be two separate reset commands, both pointing to a commit, one resetting the files on disk, one resetting the index.

It sounds to me like you want `git restore -W` and `git restore -S`.

For the compiler in particular, it's pretty common practice to depend on a recent version. The backward compatibility situation is generally excellent, so the only real challenge is getting the compiler installed. That's generally straightforward with rustup, and Debian/Ubuntu also package several versions (under different package names) that are more recent than the distro's default.

Well, for one thing, classic Outlook's HTML rendering engine (to the extent it's even an "HTML" rendering engine instead of a Word document rendering engine) has some fairly radical divergences from standard HTML/CSS behavior. For those of us who have to generate HTML e-mail for whatever reason, it's distinctly unpleasant.

That might not be enough reason in and of itself to throw out the whole application, but I sure won't miss that one part of Outlook.

The logical model is identical for every source control system.

Most source control systems have some common logical concepts (e.g. files and directories), but there's actually significant divergence between their logical models. For instance:

- Classic Perforce (as opposed to Perforce Streams) has a branching model that's very different from Git's; "branches" are basically just directories, and branching/merging is tracked on a per-file basis rather than a per-commit basis. It also tracks revisions by an incrementing ID rather than hashes. - Darcs and Pijul represent the history of a file as an unordered set of patches; a "branch" is basically just a set of patches to apply to the file's initial (empty) state.

All of that is above the physical state, which also differs:

- Perforce servers track files' revision histories in a directory hierarchy that mirrors the repository's file structure rather than building a pseudo-directory hierarchy over a flat object store. - Fossil stores everything in an SQLite database.

Is that relevant to something?

Yes. You can use a VCS reasonably effectively if you understand its logical model but not its physical storage model. It doesn't work so well the other way around.

Especially with Vue 3, most of the reactive state management you need for all but the largest and most complex of apps is built right into the core. Vuex and Pinia really aren't necessary for most users.

I've got both a Metropolitan and a Prera, and I can second (third?) this recommendation. Fantastic pens.

For the specific use case you're describing, I've found `git worktree` to be very useful.

Overall, at least when it comes to features, I've actually had the opposite experience. That said, I'm one of those people such that you can pry `git rebase` out of my cold, dead hands, so it's that feature specifically that I really miss when I have to work with Perforce.

By mainstream Christian standards, the doctrine is, quite frankly, "heretical", but members of The Church of Jesus Christ of Latter-day Saints (the primary branch of "Mormonism") very much consider themselves Christian, have values that align strongly with many other Christian denominations, place Christ at the center of their doctrine, etc. In my opinion, culturally, we're a lot more Christian than the typical perception would suggest.

That being said, we've got our own offshoot groups that we don't consider "really Mormon", so I get where people are coming from on that question.

O Holy Crap 4 years ago

In my experience, this often indicates a poorly-fitting cut. If your belt naturally sits below your hips, you'll probably want jeans specifically designed for that.

The same thing applies to any binaries downloaded from their site, so unless you you've got signed binaries (that use an independently obtained/verified chain of trust), trusting the server is your your only option. Even with signed binaries, you're still trusting the entity that holds the signing key.

Sure, in many cases. Depending on the situation, though, it may be no more hassle than any other USB GPIO device, so it's a decent enough cheap solution if you can't or don't want to use a Pi or a similar SBC.

Your experience with NixOS mirrors mine. I got hibernation working with a little fiddling. Suspend-then-hibernate also mostly works but occasionally turns my laptop sleeve into an Easy-Bake oven, so I've started triggering hibernation manually before I put it away. Plasma's touch pad settings help a bit with the extra clicks, but a little extra palm rejection is something I wouldn't complain about. Pretty happy overall, though.

When you follow the restrictions on stackless coroutines, the coroutine can, as you mentioned, elide stack allocations for the "child" coroutines. If you want to make a call that can't have its stack allocation elided, you explicitly tell the runtime to spawn it as a top-level task.

I mean, with "traditional" coroutines, it isn't the "language's runtime" which calls back into my code: it is whatever code completed the event. I get that the important part of this sentence is the interest in "push" vs. "poll", but this concept of the existence of a "language's runtime" is a bit strange to me, as my mental model of a coroutine doesn't involve a "runtime" and certainly doesn't involve an "executor".

Syntactically, many languages represent the operation of calling into the next continuation as a regular return (for green threads) or a regular function call (call/cc), but there's always some degree of runtime magic involved in the generated code. For instance, rather than just incrementing or decrementing the stack pointer, you've got to potentially set it to point into a totally different runtime-allocated stack. In principle, that can probably be implemented as just special-case code generation rather than an actual call into the runtime's routines, but that still leaves the need to clean up the current task's stack after it returns (or does a tail call into another stack), which will be either an explicit runtime call or rely on the runtime's garbage collector.

The real magic, though, isn't so much in the user-written continuations as it is on "async blocking" calls for things like I/O.

In a more complex scenario, the function A is going to do something mysterious and later get a callback from something -- which you might call a "runtime" but which almost certainly isn't implemented by the "language" -- on some random background thread running an I/O loop, or maybe due to a signal / handler from the operating system, or whatever random mechanism it has in place to run code later (which again: isn't part of the "language") and it will run the continuation it was passed.

This is precisely what Rust's async runtime libraries are. They provide the event loop/callback mechanisms, which are necessary for truly async code. (Otherwise, what is there to wait for?) You can totally write and call an async function in Rust that doesn't use a runtime, but there's no way for it to "asynchronously block"; you'd just poll it and get back, "Yep, I'm done; here's my result."

This does, likely, result in some heap allocation somewhere in order to type erase the continuation in the general case. However, this seems to only be due to how the asynchronous code has been given a harder challenge of dealing with arbitrarily deep stacks with minimal overhead, while people seem totally OK with synchronous code causing random stack overflows :/. If you are willing to relax that assumption a bit then you can elide that allocation almost every time.

It's not just the depth of the stack; it's that, once you yield, another task may take over the thread's flow control entirely. Let's say that function A spawns a coroutine B without waiting for it to finish. Now let's imagine that B allocates space on the same thread stack that A was using (on top of A's stack frame) and then yields. At the yield point, something (e.g. the runtime) has to say, "OK, B is stuck, so what do we run next on the thread?" Eventually, it's A's turn to finish running, and it returns. If it does this naïvely, it'll rewind the stack pointer, dropping the stack frames for both A and B. But B isn't done running yet; it's just blocked, so now we've got a problem because its stack just got clobbered. To avoid this, languages that use "stackful" coroutines have to allocate coroutines' stacks on the heap in many cases because the traditional single-stack model isn't just running out of space; it totally breaks down.

Rust uses stackless coroutines, which impose some restrictions on how the coroutine is structured (mostly involving unbounded recursion) so that the state the task has to store between yield points has a fixed size.

You might think of Rust's async paradigm as "half a continuation, turned upside down". With traditional coroutines, after an async operation completes, the language's runtime calls back into your code, and you actively call the next thing, "pushing" control flow down the pipe. Most languages with continuations manage this by "pausing" your function and keeping its stack frame around, which, in the general case, means your function's stack frame has to be heap-allocated, which is basically the language itself giving you a "pseudo-thread". You eventually get control back with the same stack frame, and as far as the language is concerned, how you get back there is none of your concern; that's its job.

In Rust's polling-based model, there's no "magic" saving of stack frames. You get some space to store state, but the runtime has to manage that memory itself. You can use the language to express "this is the next thing to call", but when you spawn an async I/O task and yield to it, you've already returned from your own function to the runtime, and it's the runtime's job to call your function again with the state it had stashed away. You then jump over the steps in your function that have already been handled and call into the next thing. It gets a bit more involved due to various bits of syntactic sugar, but that's the basic model. It's operating at a lower level of abstraction than many languages' coroutines or call/cc, which gives you the flexibility to customize the behavior to meet specific needs.

A runtime for generic desktop/server apps may maintain a thread pool and call back into your code on one of those threads. In WebAssembly, execution is single-threaded, but JavaScript promises may call into your runtime, and you have to dispatch that to the right Rust future. On embedded platforms, the data structures that the desktop/server runtime uses may simply not be suitable (e.g. because you have no general-purpose heap allocator), so you need to use a different approach with more constraints.

Interoperability between these runtime is possible. The key is that you need a task that's running on one runtime to be able to spawn a task on the other, with part of that task's job being to notify the first runtime that it's time to poll the "parent" task again. The mechanics vary depending on how each runtime handles task spawning.

As I understand it (from having skimmed some articles a while back), C++'s co_await isn't really all that different. Since we don't have the executors proposal as part of the standard yet, it's still a "bring-your-own runtime" sort of approach, with some kind of glue required at the boundaries between runtimes. Depending on which "flavor" of C++ coroutines you're using (e.g. push-based vs. pull-based), that interop might be easier than Rust's at the cost of other tradeoffs (e.g. more heap allocations).