HNHacker News
TopNewBestAskShowJobs

NobodyNada

3,658 karma · joined May 9, 2019

submissionscomments
NobodyNada··on Starship's Tenth Flight Test
...Huh? Why would that be sad?

My parents have formative memories of watching the moon landings as kids, and I of watching the space shuttle. As someone who ended up where I am in large part due to my curiosity about the world growing up, reading of OP's kid watching a rocket launch, thinking critically about what they're seeing, and learning more about the world from it is a joyous thing.

NobodyNada··on Ask HN: Why hasn't x86 caught up with Apple M series?
> Apple CPU's are even optimized for Apple software GC calls like Retain/Release objects.

I assume this is referring to the tweet from the launch of the M1 showing off that retaining and releasing an NSObject is like 3x faster. That's more of a general case of the ARM ISA being a better fit for modern software than x86, not some specific optimization for Apple's software.

x86 was designed long before desktops had multi-core processors and out-of-order execution, so for backwards compatibility reasons the architecture severely restricts how the processor is allowed to reorder memory operations. ARM was designed later, and requires software to explicitly request synchronization of memory operations where it's needed, which is much more performant and a closer match for the expectations of modern software, particularly post-C/C++11 (which have a weak memory model at the language level).

Reference counting operations are simple atomic increments and decrements, and when your software uses these operations heavily (like Apple's does), it can benefit significantly from running on hardware with a weak memory model.

NobodyNada··on Optimizing our way through Metroid
There have definitely been some applications of this sort of thing to speedrunning -- though far less sophisticated than the approach here, and usually only testing against a very small subset of the game. I've heard of some of this kind of work being done before on e.g. SM64.

I've also done something along these lines myself in Super Metroid. Mother Brain's neck moves in a conceptually simple but very chaotic pattern influenced by Samus's vertical movement, and there's a cutscene during the fight where the positioning of her neck can make a difference of about 7 seconds. The TAS fight used complicated movement to manipulate her neck position developed through much trial-and-error, while the best known human-viable manips were several seconds slower.

I wrote a program to search the state space for optimal movement patterns, and working with some speedrunners we were able to come up with a new human-viable manipulation that matched the previous TAS fight, as well as a new TAS manipulation that saved an additional 41 frames.

https://youtu.be/7SHD9L_Jx5Q

https://github.com/NobodyNada/mbsim

NobodyNada··on It’s not wrong that "\u{1F926}\u{1F3FC}\u200D\u2642\uFE0F".length == 7 (2019)
That works! (The braces are droppable for 16-bit codepoints in JS, but required in Rust.)
NobodyNada··on It’s not wrong that "\u{1F926}\u{1F3FC}\u200D\u2642\uFE0F".length == 7 (2019)
That would be "\U0001F926\U0001F3FC\u200D\u2642\uFE0F" in Python's syntax, or "\u{1F926}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}" in Rust or JavaScript.

Might be a little long for a title :)

NobodyNada··on Why are anime catgirls blocking my access to the Linux kernel?
The math in the article assumes scrapers only need one Anubis token per site, whereas a scraper using 500,000 IPs would require 500,000 tokens.

Scaling up the math in the article, which states it would take 6 CPU-minutes to generate enough tokens to scrape 11,508 Anubis-using websites, we're now looking at 4.3 CPU-hours to obtain enough tokens to scrape your website (and 50,000 CPU-hours to scrape the Internet). This still isn't all that much -- looking at cloud VM prices, that's around 10c to crawl your website and $1000 to crawl the Internet, which doesn't seem like a lot but it's much better than "too low to even measure".

However, the article observes Anubis's default difficulty can be solved in 30ms on a single-core server CPU. That seems unreasonably low to me; I would expect something like a second to be a more appropriate difficulty. Perhaps the server is benefiting from hardware accelerated sha256, whereas Anubis has to be fast enough on clients without it? If it's possible to bring the JavaScript PoW implementation closer to parity with a server CPU (maybe using a hash function designed to be expensive and hard to accelerate, rather than one designed to be cheap and easy to accelerate), that would bring the cost of obtaining 500k tokens up to 138 CPU-hours -- about $2-3 to crawl one site, or around $30,000 to crawl all Anubis deployments.

I'm somewhat skeptical of the idea of Anubis -- that cost still might be way too low, especially given the billions of VC dollars thrown at any company with "AI" in their sales pitch -- but I think the article is overly pessimistic. If your goal is not to stop scrapers, but rather to incentivize scrapers to be respectful by making it cheaper to abide by rate limits than it is to circumvent them, maybe Anubis (or something like it) really is enough.

(Although if it's true that AI companies really are using botnets of hacked computers, then Anubis is totally useless against bots smart enough to solve the challenges since the bots aren't paying for the CPU time.)

NobodyNada··on Pixel 10 Phones
It's impossible to prove a negative, like "Apple doesn't have a backdoor". One can prove the existence of a backdoor by reverse-engineering suspicious code or network traffic, but not the nonexistence without poring over every byte of machine code, and quite a lot of the hardware too.

This is not unique to Apple, it's impossible to prove any system is free of a backdoor, including Linux distributions (see: the xz backdoor, or "Reflections on trusting trust"), unless you hand-crafted your whole smartphone from raw silicon.

NobodyNada··on Why are anime catgirls blocking my access to the Linux kernel?
My understanding is that AI scrapers rotate IPs to bypass rate-limiting. Anubis requires clients to solve a proof-of-work challenge upon their first visit to the site to obtain a token that is tied to their IP and is valid for some number of requests -- thus forcing impolite scrapers to solve a new PoW challenge each time they rotate IPs, while being unobtrusive for regular users and scrapers that don't try to bypass rate limits.

It's like a secondary rate-limit on the ability of scrapers to rotate IPs, thus allowing your primary IP-based rate-limiting to remain effective.

NobodyNada··on Branch prediction: Why CPUs can't wait?
> If a program over the course of a few thousand instructions hits a branch X% of the time, then X will be the branch prediction.

This is not completely true - modern branch predictors can recognize patterns such as "this branch is taken every other time", or "every 5th time", etc. They also can, in some cases, recognize correlations between nearby branches.

However, they won't use factors like register or memory contents to predict branches, because that would require waiting for that data to be available to make the prediction -- which of course defeats the point of branch prediction.

NobodyNada··on We rewrote the Ghostty GTK application
> Rust has no magical ways of turning unsafe code safe

Fully agreed on that -- the unsafe keyword means it's unsafe.

> nor is it in any way special in being able to create statically-verifiable abstractions around an unsafe core.

This isn't really true. Rust's type system has:

- Affine types

- Lifetimes

- Borrowed and exclusive references

- Thread-safety

- Explicit delineation between safe and unsafe code -- with no UB in safe code, and a (cultural) design principle that unsafe code is not allowed to make unchecked assumptions about the behavior of safe code

Most mainstream languages do not have any one of these features, let alone all of them. Thus, Rust's ability to "create statically-verifiable abstractions around an unsafe core" is in practice far more powerful than in C, C++, or Zig. You certainly have the ability to create some such abstractions in other languages, but you cannot statically verify nearly as many properties. (And of course there are languages like SPARK that can verify even more statically than Rust.)

In my experience, as a systems programmer who heavily uses Rust to interact with FFI, hardware MMIO and DMA, interrupt handling, networking code, etc., Rust's ability to safely abstract unsafe primitives is easily the most practically useful aspect of the language. It's not a silver bullet that turns incorrect code into correct code, but it is incredibly good at verifying the correctness of a large application built from small low-level primitives. If you're interfacing with buggy C code, Rust certainly isn't going to help you much -- but it does makes a huge difference in preventing "you're holding it wrong" bugs when interacting with a C API.

An unsafe block declares an axiom: you're asserting to the compiler that you've verified (statically with the type system or dynamically with runtime assertions) all preconditions necessary for some primitive to be sound (free of UB). The compiler can then use that axiom to prove the soundness of all code that interacts with that primitive. When I write a driver to perform a DMA transfer, I only have to think through all the concurrency, alignment, lifetime, moveability, caching, and cancellation requirements once, and the compiler will check them for me every time I (or anyone else) use that driver in an application.

NobodyNada··on We rewrote the Ghostty GTK application
In general: whenever you call into C, you get all the safety of C, because C code can trivially have memory corruption bugs that corrupt memory "belonging" to Rust as well, inducing undefined behavior across your whole program [0].

In addition, it's often considered permissible for C APIs to exhibit undefined behavior if their API contracts are violated. This means that a bug in Rust code that calls into a C API incorrectly can (indirectly) cause undefined behavior. For this reason, all FFI calls are marked unsafe in Rust.

The typical approach to using C libraries from Rust is to create a "safe wrapper" around the unsafe FFI calls that uses Rust's type system and lifetimes to enforce the safety invariants. Of course it's possible to mess up this wrapper and have accidental undefined behavior, but you're much less likely to do so through a safe wrapper than if you use the unsafe FFI calls directly (or use C or Zig) for a couple reasons:

- Writing the safe wrapper forces you to sit down and think about safety invariants instead of glossing over it.

- Once you're done, the compiler will check the safety invariants for you every time you use the API -- no chance of making a mistake or forgetting to read a comment.

[0]: This could be avoided/mitigated with some kind of lightweight in-process sandboxing (e.g. Intel MPK + seccomp) to prevent C libraries from accessing memory that they don't own or performing syscalls they shouldn't. There's some academic research on this (and I experimented with it myself for a masters thesis project), but it generally requires some (minimal) performance overhead and code changes at language boundaries.

NobodyNada··on The Article in the Most Languages
This came up on HN a few months ago, when someone posted a list of most-translated articles and Woodard was at the top: https://news.ycombinator.com/item?id=44031697

It looks like a user in the HN thread noticed the irregularities on the Italian Wikipedia [0] and started the deletion discussion [1] that the article credits with kickstarting this investigation.

[0]: https://news.ycombinator.com/item?id=44035222

[1]: https://it.wikipedia.org/wiki/Wikipedia:Pagine_da_cancellare...

NobodyNada··on Zig's Lovely Syntax
There's a huge semantic difference: a type with zero allowed values can never be constructed.

This means that a function that returns an uninhabited type is statically known to not return -- since there's no way it could construct an uninhabited value for a return expression. It also means you can coerce a value of uninhabited type into any other type, because any code which recieves a value of an uninhabited type must be unreachable.

For instance, in Rust you can write the following:

    let weekday_name: &str = match weekday_number {
        1 => "Sunday",
        2 => "Monday",
        3 => "Tuesday",
        4 => "Wednesday",
        5 => "Thursday",
        6 => "Friday",
        7 => "Saturday",
        i => panic!("invalid weekday number: {i});
    };
Because panic! aborts the program and doesn't return, its return type is uninhabited. Thus, a panic! can be used in a context where a string is expected.
NobodyNada··on Fast
My eyes seem to navigate by paragraph more so than by line. It's hard to try to overanalyze how I read, but I think "corners" of a paragraph are landmarks that I latch onto, and when I reach the end of a line of text I don't scan back along the the line horizontally back to the left, I "jump" back, using the boundaries of the paragraph to estimate the start of the next line, and continue reading from there.

This means that I have a difficult time reading text with very large paragraphs. If a paragraph goes on for 10+ lines, I'll start to lose my place at the end of most lines. This is infuriating and drastically impairs my ability to read and comprehend the text.

It's interesting to me that you mention preferring a ragged right over justification, because I literally do not notice the difference. This suggests to me that we read in different ways -- perhaps you focus on the shape and boundaries of a line more than the shape of a paragraph. This makes intuitive sense to me as to why you would prefer narrower columns.

I don't think that I'm "right" for preferring wider columns or that you or anyone else are "wrong" for preferring narrower columns. I think it's just how my brain learned to process text.

I have pretty strong opinions on what's too wide of a column and what's too narrow of a column, so I won't fullscreen a browser window on anything larger than a laptop. Rather, I'll set it for a size that's comfortable for me. If some web designer decides "actually, your preferred text width is wrong, use mine instead" then I'm gonna be pretty annoyed, and I think rightfully so, because what those studies say is "optimal" for the average person is nigh unreadable for me. (Daring Fireball is the worst offender I can think of off the top of my head. I also find desktop Wikipedia's default view pretty hard to read, but the toggleable "wide" mode is excellent).

NobodyNada··on Fast
A narrow column of text can make it easier to read individual sentences, but it does so by sacrificing vertical space, which makes it harder to skim a page for relevant content and makes it easier for me to lose track of my place since I can't see as much context, images, and headings on screen all at once. I also find it much harder to read text when the paragraphs form monotonous blocks spanning 10 lines of text rather than being irregularly shaped and covering 3-5 lines. I find Wikipedia articles much harder to read in "standard" mode compared to "wide" mode for this reason.

Different people process visual information differently, and people reading articles have different goals, different eyesight, and different hardware setups. And we already have a way for users to tell a website how wide they want its content to be: resizing their browser window. I set the width of my browser window based on how wide I want pages to be; and web designers who ignore this preference and impose unreadable narrow columns because they read about the "optimal" column width in some study or another infuriate me to no end. Optimal is not the same for everyone, and pretending otherwise is the antithesis of accessibility.

NobodyNada··on Resizable structs in Zig
Or function pointers (especially given that Zig's been moving towards encouraging vtables over static dispatch)?
NobodyNada··on Using uninitialized memory for fun and profit (2008)
> there was some kind of optimization in LLVM that relied on being able to assume values were never undef

It's true that LLVM has restrictions on what you can do with undef/poison memory, but LLVM also supports the "freeze" operation that comes up in the Rust discussions (which transforms an undefined value into an arbitrary, well-defined value). It would certainly need to be unsafe to avoid violating invariants like you mentioned, but "LLVM" isn't the blocker to supporting this.

Rather, there are more subtle problems with reading from initialized memory -- for example on Linux, a heap allocator might use MADV_FREE on free memory, which hints to the kernel that a page contains freed memory and the operating system is not required to preserve its contents until the application writes to it again. This means the following sequence of events is possible:

- An application frees some memory, and the heap allocator invokes madvise(MADV_FREE) on the address range.

- The application makes a heap allocation, obtaining a pointer to the free'd memory.

- The application freezes the uninitialized memory and reads from it.

- Due to memory pressure, the kernel decides to reclaim the free'd memory. It unmaps it from the process and uses it somewhere else.

- The application accesses the first allocation again, and sees that its value has now changed to all-zeroes.

Thus, we can see that "freezing" arbitrary memory can't actually be implemented on real-world systems -- the contents of uninitialized memory really can change out from under you until you write to that memory.

It would be possible to implement a "by-reference freeze" that copies a MaybeUninit<T> to a new location, but introducing this functionality still has the downside that you can write a Heartbleed bug without invoking undefined behavior, which is what makes it controversial.

NobodyNada··on Zig's New Async I/O
In larger Rust applications or servers I find myself doing this very often -- for example, one application I'm working on mostly uses blocking I/O for occasional filesystem access but has a little bit of async networking.
NobodyNada··on Tree Borrows
As someone who has been writing a lot of unsafe Rust (mostly in an embedded context), I'm thrilled about and thankful for the work that you, your students, and the opsem team are doing.

When you're working with anything below the application level, C's confusing and underspecified rules about UB are almost impossible to keep track of, especially when it comes to aliasing and volatile/MMIO. The spec is so difficult to read and full of complicated cross-references that to actually get a practical answer you have to look for a random Stack Overflow post that may or may not have a correct interpretation of the spec, and may or may not address your specific problem.

Rust right now feels a lot harder to work with, because the spec isn't done. When you have a concrete question about a piece of code, like "is this conversion from an &mut to a *mut and back sound", and you try to look for documentation on it, you get either "Nobody knows, Rust aliasing model isn't defined"; a hand-wavy explanation that is not rigorous or specific; or a model like Stack Borrows or Tree Borrows that's defined a little too formally for easy digestion :)

But when I really started digging, I realized just how much cleaner Rust's semantics are. References aren't actually hard, Tree Borrows basically boils down to "while an &mut reference is live, you can only access the value through pointers or references derived from that reference". Pointer operations have straightforward semantics, there's no confusing notions of typed memory, and no UB "just because" for random things like integer overflow. It's just so much less complicated to understand than C's abstract machine.

I'm really looking forward to things like MiniRust, and to an aliasing model making it into the Reference / other documentation, because at that point I feel like unsafe Rust will be way easier to write confidently and correctly than C.

Congrats on the publication, and thanks again for the work you all have put into this.

NobodyNada··on Donkey Kong Country 2 and Open Bus
It's mostly the audio clock that is suspectible to drift. Everything except the audio subsystem is derived from a single master clock, so even if the master clock varies in frequency slightly, all the non-audio components will remain in sync with each other.

That means the 2 clock cycles could theoretically make an observable difference if they cause the CPU to miss a frame deadline and cause the game to take a lag frame. But this is rather unlikely.

NobodyNada··on Donkey Kong Country 2 and Open Bus
Open bus quite literally means that the data bus lines are an open circuit -- the CPU has placed an unmapped or write-only address on the address bus, and no hardware on the bus has responded, so the bus lines aren't being driven and are just floating. Thus, nominally, this is a case of undefined behavior at the hardware level.

In order to understand what actually happens, we need to look a little closer at the physical structure of a data bus -- you have long conductors carrying the signals around the motherboard and to the cartridge, separated from the ground plane by a thin layer of insulating substrate. This looks a lot like a capacitor, and in fact this is described and modeled as "parasitic capacitance" by engineers who try to minimize it, since this effect limits the maximum speed of data transmission over the bus. But this effect means that, whenever the bus is not being driven, it tends to stay at whatever voltage it was last driving to -- just like a little DRAM cell, producing the "open-bus reads return the last value transferred across the bus" effect described in the article.

It's not uncommon for games to accidentally rely on open-bus effects, like DKC2. On the NES, the serial port registers for connecting to a controller only drive the low-order bits and the high bits are open-bus; there are a few games that read the controller input with the instruction LDA $4016 and expect to see the value $40 or $41 (with the 4 sticking around because of open-bus).

There's also speedrun strategies that rely on open-bus behavior as part of memory-corruption or arbitrary-code-execution exploits, such as the Super Mario World credits warp, which sends the program counter on a trip through unmapped memory before eventually landing in RAM and executing a payload crafted by carefully manipulating enemy positions [1].

But there's some exceptions to the usual predictable open bus behavior. Nonstandard cartridges could return a default value for unmapped memory, or include pull-up or pull-down resistors that impact the behavior of open bus. There's also an interesting interactions with DMA; the SNES supports a feature called HDMA which allows applications to schedule DMA transfers to transfer data from the CPU to the graphics hardware with precise timing in order to upload data or change settings mid-frame [2]. This DMA transfer temporarily pauses the CPU in order to use the bus to perform the transfer, which can change the behavior of an open-bus read if a DMA transfer happens to occur in the middle of an instruction (between reading the target address & performing the actual open-bus read).

This very niche edge case has a significant impact on a Super Metroid speedrun exploit [3] which causes an out-of-bounds memcpy, which attempts to transfer a large block of data from open-bus to RAM. The open-bus read almost always returns zero (because the last byte of the relevant load instruction is zero), but when performed in certain rooms with HDMA-heavy graphical effects, there's a good chance that a DMA transfer will affect one of the reads, causing a non-zero byte to sneaks in somewhere important and causing the exploit to crash instead of working normally. This has created a mild controversy in the community, where some routes and strategies are only reliable on emulators and nonstandard firmwares; a player using original hardware or a very accurate emulator has a high chance of experiencing a crash, whereas most emulators (including all of Nintendo's official re-releases of the game) do not emulate this niche edge case of a mid-instruction HDMA transfer changing the value of an open-bus read.

Also, the current fastest TAS completion of Super Metroid [4] relies on this HDMA interaction. We found a crash that attempted to execute open bus, but wasn't normally controllable in a useful way; by manipulating enemies in the room to influence CPU timing, we were able to use HDMA to put useful instructions on the bus at the right timing, eventually getting the console to execute controller inputs as code and achieve full arbitrary code execution.

[1]: https://youtu.be/vAHXK2wut_I

[2]: https://youtu.be/K7gWmdgXPgk

[3]: https://youtu.be/CnThmKhtfOs

[4]: https://tasvideos.org/8214S

NobodyNada··on Rust in the Linux kernel: part 2
> Some drivers written in Rust instead of C would need to be littered with unsafes, raw pointers, pointer arithmetic,

The main safety benefit of Rust in a low-level environment is not that you never have to write unsafe code; it's that the language allows you to encapsulate unsafe code and express its safety invariants in the type system. My go-to example of this is the Vec type in the standard library. The language doesn't have any notion of a growable array built-in; it's defined in pure Rust code in the standard library using some unsafe code that exposes a fully safe API. And using the invariants expressed in that API, the compiler can prove the safety of any user code that uses a growable array, under the axiom that the unsafe definition of Vec was correct.

The standard library is rich enough that application developers never really have to touch unsafe, and that's great for them -- but for those of us doing systems programming, this "axiomatic safety" (as I like to think of it) is the real power of Rust. We can do complicated and unsafe things (like direct hardware access, complicated pointer tricks, concurrent data structures, etc.), write out the invariants users need to uphold in order for the code to be sound, and then the compiler can prove the program safe under the assumption that our unsafe building blocks are sound. Well-written Rust isn't "littered with unsafes" everywhere; the unsafe code only appears in low-level definitions of data structures and hardware/foreign function interfaces, and the rest of the code is written in safe Rust.

> manual memory management, manual layout control, and manual resource management to retain similar behaviors.

All of these things are manual in Rust. There's no GC and the compiler does not insert allocations or deallocations; it's all done by user or library code. Layout is entirely controlled by the programmer (the compiler does optimize struct layout by default, but you can opt out by marking the struct #[repr(C)]).

Rust is not garbage collected, and there is no automatic memory management of any kind. The philosophy is: you manage memory manually, and the compiler checks your work to make sure you didn't make a mistake. The only real difference from C (besides the safety guarantees) is that Rust has destructors, so you can use RAII to simplify cleanup/freeing.

NobodyNada··on Weird Expressions in Rust
Yes, exactly -- it's called the Never type.

It's also useful in more places than return expressions -- for example, you can make a function return ! to indicate that it's a non-returning function, which is useful for expressing, say, an error handler that must crash the program; or a main loop that must never return. It also can help the compiler generate more compact code when a function is known to not return.

There's currently work in progress to allow you to specify ! as a type everywhere, not just as function returns. This is useful where some generic code expects a function to return a Result with an implementation-specified error type, since an infallible implementation can specify ! as the error type. Then, the type checker can allow the programmer to unwrap a Result<T, !> without checking for errors, and the optimizer can remove the error-checking branches from generic code: https://doc.rust-lang.org/std/primitive.never.html

This has taken a very long time to implement, because of some very subtle implications on type inference that made it difficult to stabilize without breaking compatibility -- but the 2024 edition finally figured out a way to make it possible.

NobodyNada··on Weird Expressions in Rust
In practice, it's quite a useful feature, because you can write things like this:

    let day_number = match name {
        "Sunday" => 0,
        "Monday" => 1,
        "Tuesday" => 2,
        "Wednesday" => 3,
        "Thursday" => 4,
        "Friday" => 5,
        "Saturday" => 6,
        _ => return Err("invalid day")
    };
NobodyNada··on A federal judge sides with Anthropic in lawsuit over training AI on books
One aspect of this ruling [1] that I find concerning: on pages 7 and 11-12, it concedes that the LLM does substantially "memorize" copyrighted works, but rules that this doesn't violate the author's copyright because Anthropic has server-side filtering to avoid reproducing memorized text. (Alsup compares this to Google Books, which has server-side searchable full-text copies of copyrighted books, but only allows users to access snippets in a non-infringing manner.)

Does this imply that distributing open-weights models such as Llama is copyright infringement, since users can trivially run the model without output filtering to extract the memorized text?

[1]: https://storage.courtlistener.com/recap/gov.uscourts.cand.43...

NobodyNada··on (On | No) Syntactic Support for Error Handling
Rust tends to prefer reusing keywords (in unambiguously different contexts) rather than introducing new ones, because adding a new keyword is a backwards compatibility break that requires a new language edition and creates migration pain for anyone who had an identifier with the same name as the new keyword.
NobodyNada··on (On | No) Syntactic Support for Error Handling
`map_err` does not need to be type-directed; you can use an arbitrary function or closure. An enum variant can be used as a function mapping from the variant type to the error type, but we can do any arbitrary transformation:

    .map_err(|e| format!("Failed to read file: {e}")?;
But the "idiomatic Go" way of doing things sounds a lot closer to anyhow in Rust, which provides convenience utilities for dealing with type-erased errors:

    use anyhow::{Result, Context};

    fn foo() -> Result<()> {
        let data = std::fs::read("/some/file").context("Failed to read file")?;
        // ...
    }
NobodyNada··on (On | No) Syntactic Support for Error Handling
> Since you could just design your `?` to encourage wrapping instead.

Which is exactly what Rust does -- if the error returned by the function does not match the error type of `?` expression, but the error can be converted using the `From` trait, then the conversion is automatically performed. You can write out the conversion implementation manually, or derive it with a crate like thiserror:

    #[derive(Error)]
    enum MyError {
        #[error("Failed to read file")
        IoError(#[from] std::io::Error)
        // ...
    }

    fn foo() -> Result<(), MyError> {
        let data = std::fs::read("/some/file")?;
        // ...
    }
You can also use helper methods on Result (like `map_err`) for inserting explicit conversions between error types:

    fn foo() -> Result<(), MyError> {
        let data = std::fs::read("/some/file").map_err(MyError::IoError)?;
        // ...
    }
NobodyNada··on Programming on 34 Keys (2022)
It's not about how fast you can type, it's about how effortlessly you can type.

Programming involves long periods of thinking interrupted by short periods of typing. Anything you can do to reduce the mental effort of typing reduces the impact of this interruption to your train of thought.

Try programming by hunt-and-peck typing -- it's certainly possible, but having to think about putting each individual character on the screen is incredibly tedious and distracting. This is why people learn things like Vim motions or minimalist keyboard layouts -- anything that speeds up the feedback loop between your brain and your code is an improvement. It's not a drastic difference, but it's significant enough that I now feel mildly annoyed using a regular keyboard when I have to move my whole arm to reach the arrow keys rather than having them right under my fingers on the home row.

NobodyNada··on The scientific “unit” we call the decibel
Consider the signal chain of a live audio system, from a singer to a PA.

The singer produces sound pressure waves into a mic. These pressure waves are measured in dB SPL, and the microphone's specifications will tell you its sensitivity, maximum sound pressure level, frequency response, on- and off-axis response, and so on. An engineer can look at all these specs to choose a mic based on the environment, how much background noise there is and where it's coming from, the quality and character of the singer's voice, etc.

The microphone will produce a voltage, which is measured in dBV or dBu. This voltage travels to a preamplifier, which boosts the signal to a nominal level by applying a gain (in dB, with no units since it's just a dimensionless ratio between input level and output level) and converts it to a digital signal -- measured in dBFS ("decibels full-scale", signal level relative to the maximum level that can fit in the bit-width of the digital signal).

Note that all the different signals going into the preamps have wildly different voltage levels -- a kick drum produces much more sound energy than a singer. And the ratio between SPL and voltage is not fixed; different mics have different sensitivities, and an active guitar amp will generate a signal many orders of magnitude stronger than a passive microphone. For some instruments like synthesizers, "SPL" isn't even a concept that makes sense because the sound is produced entirely electronically, rather than by capturing a mechanical wave. So, the preamps are configured to normalize all the incoming signals to one nominal level for processing.

After the preamp, the signal goes through a digital signal processing chain. Most of these processing steps will affect the level of the signal, and the amount is measured in dB (without units, since this is a dimensionless ratio between input level and output level. Remember, we're dealing with a fully digital waveform, so there is not even a physical measure of "loudness" or "signal strength" that can apply to the signal at this point.)

The signals from all the different sound sources are mixed together, and the mixer's per-channel volume faders are marked in decibels -- usually from +10 to -infinity, with 0 corresponding to "unity gain" (which means the output signal should have the same intensity as the input signal). Again, no units because we aren't measuring a physical quantity, just how much the mixer should change the intensity of the signal.

Finally, the signal is converted back to an analog voltage (measured in dBu) and sent to a power amplifier. The power amplifier applies an adjustable gain, and outputs a signal measured in watts (dBm) for sending to the PA system. You might then walk around the room with an SPL meter to ensure the sound is at a safe level; the value you measure will depend on the frequency and directional response of the speakers, as well as how far away from the speaker you're measuring.

Throughout this process we had signals in the form of sound pressure, voltage, digital samples, and wattage, at power levels ranging from microwatts to kilowatts. Even a simple physical quantity like sound pressure for a single signal is not straightforward -- how far are you measuring it from, and are you talking about SPL before the microphone or after the speaker?

The fact that a single unit is used for all of these different purposes is a feature, not a bug. If my preamp is close to clipping and I turn it down by 3 dB, how much do I need to turn up the gain at my compressor to compensate? Easy -- 3 dB. If we used more "appropriate" physical units at each step of the signal chain, it would be impossible to determine this number quickly or in your head; and would involve taking into account even more factors I haven't mentioned (like input impedances). The fact that decibels are scaled differently depending on whether you're working with voltage or power means that 1 dB corresponds to the same change in loudness everywhere without having to remember whether it's 1 dB of voltage or power.

← PreviousPage 5 of 22Next →