Threads for jmillikin

    1. 10

      Use consistent spelling of certain words

      I disagree with this, only because it’s imperialism. I’m British, in British English I write marshalling (with two of the letter l), sanitising (-sing instead of -zing except for words ending in a z), and -ise instead of -ize, among other things. You wouldn’t demand an Arabic developer to write all his comments in English for your sake for the sake of being idiomatic, would you?

      1. 14

        I’ve worked for a few companies in Germany now, about half of them with their operating language being in German. All of them had comments being written exclusively in English. I don’t know how that is in other countries, but I get the impression from Europeans that this is pretty standard.

        That said, my own preference is for American English for code (i.e. variable names, class names, etc), but British English for comments, commit messages, pull requests, etc. That’s because the names are part of the shared codebase and therefore standardised, but the comments and commit messages are specifically from me. As long as everyone can understand my British English, then I don’t think there’s much of a problem.

        EDIT: That said, most of these suggestions feel more on the pedantic end of the spectrum as far as advice goes, and I would take some of this with a pinch of salt. In particular, when style suggestions like “I tend to write xyz” become “do this”, then I start to raise eyebrows at the usefulness of a particular style guide.

        1. 4

          All of them had comments being written exclusively in English. I don’t know how that is in other countries, but I get the impression from Europeans that this is pretty standard.

          Developers in China seem to prefer Chinese to English. When ECharts was first open-sourced by Baidu most of the inline comments (and the entire README) were in Chinese:

          In Japan I feel like the tech industry is associated with English, and corporate codebases seem to use mostly English in documentation. However, many people’s personal projects have all the comments/docs in Japanese.

        2. 12

          If someone wants to force everyone to spell something the same within a language they should make sure it’s spelled wrong in all varieties, like with HTTP’s ‘referer’.

          1. 10

            The Go core developers feel so strongly about their speling that they’re wiling to change the names of constants from other APIs.

            The gRPC protocol contains a status code enum (https://grpc.io/docs/guides/status-codes/), one of which is CANCELLED. Every gRPC library uses that spelling except for go-grpc, which spells it Canceled.

            Idiosyncratic positions and an absolute refusal to concede to common practice is part and parcel of working with certain kinds of people.

            1. 2

              We’re drifting off-topic, but I have to ask: gRPC is a Google product; Go is a Google product; and Google is a US company. How did gRPC end up with CANCELLED in the first place?!

              1. 5

                When you use a lot of staff on H-1B and E-3 visas, you get a lot of people who write in English rather than American!

              2. 1

                Wait until you hear about the HTTP ‘Referer’ header. The HTTP folks have been refusing to conform to common practice for more than 30 years!

              3. 5

                You wouldn’t demand an Arabic developer to write all his comments in English for your sake for the sake of being idiomatic, would you?

                If this is something other than a private pet project of a person who has no ambition of ever working with people outside of his country? Yes, yes I would.

                1. 4

                  I believe the advice is still applicable to non-native speakers. In all companies I worked for in France, developers write code in English, including comments, sometimes even internal docs. There are a lot of inconsistencies (typically mixing US English and GB English, sometimes in the same sentence.)

                  1. 7

                    In my experience (LatAm) the problem with that is people tend to have pretty poor English writing skills. You end up with badly written comments and commit messages, full of grammatical errors. People were aware of this so they avoided writing long texts in order to limit their mistakes, so we had one-line PR descriptions, very sparse commenting, no docs to speak of, etc.

                    Once I had the policy changed for the native language (Portuguese) in PRs and docs they were more comfortable with it and documentation quality improved.

                    In Europe people are much more likely to have a strong English proficiency even as a second or third language. You have to know your audience, basically.

                    1. 1

                      full of grammatical errors

                      While I like to write paragraphs of explanation in-between code, my actual comments are rather ungrammatical, with a bit of git style verb-first, removing all articles and other things. Proper English feels wrong in these contexts. Some examples from my currently opened file:

                      ; Hide map’s slider when page opens first time

                      ;; Giv textbox data now

                      ;;Norm longitude within -180-180

                      ; No add marker when click controls

                      ;; Try redundant desperate ideas to not bleed new markers through button

                      ;; Scroll across date line #ToDo Make no tear in marker view (scroll West from Hawaii)

                      ; Try to limit view to world bound like viscosity

                      1. 3

                        Those comments would most likely look weird to a person unfamiliar with your particular dialect.

                        In a small comment it’s fine to cut some corners, similar to titles in newspapers, but we can’t go overboard: the point of these things is to communicate, we don’t want to make it even more difficult for whoever is reading them. Proper grammar helps.

                        1. 1

                          For clarification, this is not my dialect/way of speaking. But I see so many short interline comments like this, that I started thinking they feel more appropriate and make them too, now. Strange!

                  2. 2

                    of certain words

                    “If you use standard terms, spell them in a standard way” is not the same as “use only one language ever”.

                    1. 0

                      Is “chapéu” or “hat” the standard way of spelling hat in Golang? If it’s “hat”, your standard is “only use American English ever”.

                      1. 2

                        Is “hat” a standard term regularly used in the golang ecosystem for a specific thing and on the list given in the article? If not, it is not relevant to the point in the article.

                        (And even generalized: if it happens to be an important term for your code base or ecosystem, it probably makes sense to standardize on how to spell it. in whatever language and spelling you prefer. I’ve worked on mixed-language codebases, and it’d been helpful if people consistently used the German domain-specific terms instead of mixing them with various translation attempts. Especially if some participants don’t speak the language (well) and have to treat terms as partially opaque)

                        1. 2

                          If it’s “hat”, your standard is “only use American English ever”.

                          What? England had the word “hat” long before the USA existed.

                      2. 1

                        I had to solve this once. I maintain a library that converts between HTML/CSS color formats, and one of the formats is a name (and optional spec to say which set of names to draw from). HTML4, CSS2, and CSS2 only had “gray”, but CSS3 added “grey” as another spelling for the same color value, and also added a bunch of other new color names which each have a “gray” and a “grey” variant.

                        Which raises the question: if I give the library a hex code for one of these and ask it to convert to name, which name should it convert to?

                        The solution I went with was to always return the “gray” variant since that was the “original” spelling in earlier HTML and CSS specs:

                        https://webcolors.readthedocs.io/en/latest/faq.html#why-does-webcolors-prefer-american-spellings

                        1. 2

                          I thought you guys loved imperialism?

                          1. 8

                            Imperialism is like kids, you like your own brand.

                          2. 0

                            I don’t think it’s really “imperialism”—firstly, “marshaling” isn’t even the preferred spelling in the US. Secondly in countries all over the world job listings stipulate English language skills all the time (even Arabic candidates) and the practice is widely accepted because facilitating communication is generally considered to be important. Lastly, while empires certainly have pushed language standardization as a means to stamp out identities, I don’t think it follows that all language standards exist to stamp out identities (particularly when they are optional, as in the case of this post).

                            1. 1

                              “marshaling” isn’t even the preferred spelling in the US

                              What makes you say that? (Cards on the table, my immediate thought was “Yes, it is.” I had no data for that, but the ngram below suggests that the single l spelling is the (currently) preferred US spelling.)

                              https://books.google.com/ngrams/graph?content=marshaling%2Cmarshalling&year_start=1800&year_end=2022&corpus=en-US&smoothing=3&case_insensitive=true

                              1. 0

                                It’s imperialist to use social and technical pressure to “encourage” people to use American English so their own codebases are “idiomatic”.

                                1. 2

                                  I disagree. I don’t see how it is imperialism in any meaningful sense. Also “pressure” is fairly absurd here.

                            2. 4

                              This is inspiring some thoughts, thanks!

                              One minor consequence: the reason the “full name” confirmation is so effective is that it requires the operator to consider the actual context of the operation. Most --force flags I’m aware of disable all checks, and would be made safer by being replaced with e.g. --disable-checks=time-of-day,deny-production-writes.

                              1. 8

                                I’ve started to consider --force to be an anti-pattern in and of itself.

                                $ some_command regenerate_index
                                Check failed: Do not run outside of 09:00-17:00 local time (use --force to skip checks)
                                $ some_command regenerate_index --force
                                Check skipped: Do not run outside of 09:00-17:00 local time
                                Check skipped: As of yesterday admin operations in prod should have an associated ticket.
                                Check skipped: DO NOT SKIP!!! regenerate_index on kernel 5.19 causes kernel panic (INFRA-12345)
                                (connection lost)
                                %
                                
                                1. 7

                                  The other thing that really gets me is “confirmation” dialogs that don’t say exactly what action you’re confirming. For example, if you delete a sheet in Google Sheets, it says “Are you sure you want to delete this sheet?”. Which sheet? I’m sure I want to delete a sheet, but did I click on the right one? Saying “Are you sure you want to delete the sheet ‘Scratchwork’?” would be so much more helpful.

                                2. 31

                                  A good “falsehoods” list needs to include specific examples of every falsehood.

                                  1. 28

                                    Yours doesn’t! And I maintain that it’s still a good boy list.

                                    1. 2

                                      That doesn’t look to me like it’s meant to be an example of a good falsehoods list.

                                        1. 3

                                          In addition, it’s worth knowing that dogs up to 2 years of age exhibit the halting problem.

                                    2. 27

                                      I’ll make an attempt, with the caveat that this list seems so obvious to me that I’m worried I might be missing some nuance (imagine a similar list about cooking utensils with “people think knives can only be used for butter, but in reality they can also be used to cut bread, meat, and even vegetables!!!”).

                                      Sentences in all languages can be templated as easily as in English: {user} is in {location} etc.

                                      Both the substitutions and the surrounding text can depend on each other. The obvious example is languages where nouns have gender, but you might also have cases like Japanese where “in” might be へ, で, or に to indicate relative precision of the location.

                                      Words that are short in English are short in other languages too.

                                      German is the classic example of using lengthy compound words where English would use a shorter single-purpose word, “Rindfleisch” vs “beef” or “Lebensmittel” vs “food” (why yes I haven’t had lunch yet, why do you ask…?).

                                      For any text in any language, its translation into any other language is approximately as long as the original.

                                      See above – English -> German tends to become longer, English -> Chinese tends to become shorter.

                                      For every lower-case character, there is exactly one (language-independent) upper-case character, and vice versa.

                                      Turkish and German are famous counter-examples, with Turkish 'i' / 'I' being different letters, or German ß capitalizing to "SS" (though I think this is now considered somewhat old-fashioned?).

                                      The lower-case/upper-case distinction exists in all languages.

                                      Not true in Chinese, Japanese, Korean.

                                      All languages have words for exactly the same things as English.

                                      Every language has words that don’t exist in any other language. Sometimes because the concept is alien (English has no native word for 寿司), sometimes because a general concept has been subdivided in a different way (English has many words for overcast misty weather that don’t translate easily into languages from drier climates).

                                      Every expression in English, however vague and out-of-context, always has exactly one translation in every other language.

                                      I’m not sure what this means because many expressions in English don’t even have a single explanation in English, but in any case, idioms and double entendres often can’t be translated directly.

                                      All languages follow the subject-verb-object word order.

                                      If one’s English to SVO order is limited, limited too must their knowledge of literature be.

                                      When words are to be converted into Title Case, it is always the first character of the word that needs to be capitalized, in all languages.

                                      Even English doesn’t follow a rule of capitalizing the first character of every word. Title Casing The First Letter Of Every Word Is Bad Style.

                                      Every language has words for yes and no.

                                      One well-known counter-example being languages where agreement is by repeating a verb:

                                      A: “Do you want to eat lunch together?” B: “Eat.”

                                      In each language, the words for yes and no never change, regardless of which question they are answering.

                                      See above.

                                      There is always only one correct way to spell anything.

                                      Color / colour, aluminum / aluminium

                                      Each language is written in exactly one alphabet.

                                      Not sure exactly what this means – upper-case vs lower-case? Latin vs Cyrillic? 漢字 vs ひらがな カタカナ ? 简化字 vs 繁体字 ? Lots of counter-examples to choose from, Kazakh probably being a good one.

                                      All languages (that use the Latin alphabet) have the same alphabetical sorting order.

                                      Lithuanian sorts 'y' between 'i' and 'j': https://stackoverflow.com/questions/14458314/letter-y-comes-after-i-when-sorting-alphabetically

                                      Some languages special-case ordering of letter combinations, such as ij in Dutch.

                                      And then there’s the dozens of European languages that have their own letters outside the standard 26. Or diacritics.

                                      All languages are written from left to right.

                                      Arabic, Hebrew.

                                      Even in languages written from right to left, the user interface still “flows” from left to right.

                                      Not sure what “flows” means here, but applications with good RtL support usually flip the entire UI – for example a navigational menu that’s on the right in English would be on the left in Arabic.

                                      Every language puts spaces between words.

                                      Segmenting a sentence into words is as easy as splitting on whitespace (and maybe punctuation).

                                      Chinese, Japanese.

                                      Segmenting a text into sentences is as easy as splitting on end-of-sentence punctuation.

                                      English: "Dear Mr. Smith".

                                      No language puts spaces before question marks and exclamation marks at the end of a sentence.

                                      No language puts spaces after opening quotes and before closing quotes.

                                      French famously has rules that differ from English regarding spacing around punctuation.

                                      All languages use the same characters for opening quotes and closing quotes.

                                      “ ” in English,「 」in Japanese, « » in French,

                                      Numbers, when written out in digits, are formatted and punctuated the same way in all languages.

                                      European languages that use '.' for thousands separator and ',' for the fractional separator, or languages that group by different sizes (like lakh/crore in Indian languages).

                                      No two languages are so similar that it would ever be difficult to tell them apart.

                                      Many languages are considered distinct for political reasons, even if a purely linguistic analysis would consider them the same language.

                                      Languages that have similar names are similar.

                                      English (as spoken in Pittsburgh), English (as spoken in Melbourne), and English (as spoken in Glasgow).

                                      More seriously, Japanese and Javanese.

                                      Icons that are based on English puns and wordplay are easily understood by speakers of other languages.

                                      Often they’re difficult to understand even for English speakers (I once saw a literal hamburger used to signify a collapsable sidebar).

                                      Geolocation is an accurate way to predict the user’s language.

                                      Nobody who has ever travelled would think this. And yet. AND YET!

                                      C’mon Google, I know that my IP is an airport in Warsaw but I really don’t want the Maps UI to switch to Polish when I’m trying to find a route to my hotel.

                                      Country flags are accurate and appropriate symbols for languages.

                                      You can roughly gauge where you are in the world by whether the local ATMs offer “🇬🇧 English”, “🇺🇸 English”, or “🇦🇺 English”.

                                      Every country has exactly one “national” language.

                                      Belgium, Luxembourg, Switzerland.

                                      Every language is the “national” language of exactly one country.

                                      English, again.

                                      1. 14

                                        Turkish and German are famous counter-examples, with Turkish ‘i’ / ‘I’ being different letters, or German ß capitalizing to “SS” (though I think this is now considered somewhat old-fashioned?).

                                        The German ß has history.

                                        The old rule is that ß simply has no uppercase. Capitalizing it as “SS” was the default fallback rule if you had to absolutely capitalize everything and the ß would look bad (such as writing “STRAßE” => “STRASSE”). Using “SZ” was also allowed in some cases.

                                        The new rule is to use the uppercase ß: ẞ. So instead of “STRASSE” you now write “STRAẞE”.

                                        The usage of “SZ” was disallowed in 2006, the East Germans had an uppercase ß since 1957, the West German rules basically said “Uppercase ß is in development” and that was doppred in 1984 for the rule to use SS or SZ as uppercase variant. The new uppercase ß is in the rules since 2017. And since 2024 the uppercase ß is now preferred over SS.

                                        The ISO DIN 5008 was updated in 2020,

                                        This means depending on what document you’re processing, based on when it was created and WHERE it was created, it’s writing of the uppercase ß may be radically different.

                                        It should also be noted that if you’re in Switzerland, ß is not used at all, here the SS substitute is used even in lower case.

                                        Family names may also have custom capitalization rules, where ß can be replaced by SS, SZ, ẞ or even HS, so “Großman” can become “GROHSMANN”. Note that this depends on the person, while Brother Großmann may write “GROHSMANN”, Sister Großmann may write “GROSSMANN” and their mother may use “GROẞMANN” and these are all valid and equivalent.

                                        Umlauts may also be uppercased without the diacritic umlaut and with an E suffix; ä becomes “AE”. In some cases even lowercase input does the translation because older systems can’t handle special characters, though this is not GDPR compliant.

                                        No two languages are so similar that it would ever be difficult to tell them apart.

                                        Many languages are considered distinct for political reasons, even if a purely linguistic analysis would consider them the same language.

                                        If you ever want to have fun, the politics and regionality of German dialects could be enough to drive some linguists up the wall.

                                        Bavarian is recognized as a language and dialect at the same time, it can be subdivided into dozens and dozens of subdialects, which are all similar but may struggle to understand eachother.

                                        As someone who grew up in Swabian Bavaria, my dialect is a mix of both Swabian and Bavarian, I struggle to understand Northern Bavaria but I struggle much less with Basel Swiss Germany (which is distinct from Swiss German in that it originates from Lower Allemans instead of Higher Allemans) which is quite close in a lot of ways.

                                        And the swiss then double down on making things confusing by sometimes using french language constructs in german words, or straight up importing french or italian words.

                                        1. 2

                                          East Germans had an uppercase ß since 1957

                                          What should I read to learn more about this? Why wasn’t the character in Unicode 1.0, then?

                                          1. 5

                                            East Germany added the uppercase ß in 1957 and removed it in 1984. The spelling rules weren’t updated, so despite the presence of an uppercase ß, it would have been wrong to use it in any circumstances. Since Unicode 1.0 is somewhere around 1992, with some early drafts in 1988, it basically missed the uppercase ß being in the dictionary.

                                            The uppercase ß itself has been around since 1905 and we’ve tried to get it into Unicode since roughly 2004.

                                            1. 1

                                              Is this more like there being an attested occurrence in a particular dictionary in East Germany in 1957 rather than common usage in East Germany?

                                          2. 7

                                            Every expression in English, however vague and out-of-context, always has exactly one translation in every other language.

                                            I’m not sure what this means because many expressions in English don’t even have a single explanation in English, but in any case, idioms and double entendres often can’t be translated directly.

                                            A good example of this is a CMS I used to work on. The way it implemented translation was to define everything using English[0], then write translations as a mapping from those English snippets to the intended language. This is fundamentally flawed, e.g. by homonyms:

                                            Subject            From       Flags              Actions
                                            ----------------------------------------------------------------
                                            Project update     Alice      Unread, Important  [Read] [Delete]
                                            Welcome            HR         Read               [Read] [Delete]
                                            

                                            Here, the “Read” flag means “this has been read”, whilst the “Read” button means “I want to read this”. Using the English as a key forces the same translation on both.

                                            [0] We used British English, except for the word “color”; since we felt it was better to match the CSS keywords (e.g. when defining themes, etc.).

                                            1. 4

                                              One trick is to use a different word on the asset: Reviewed(adj) and Review(v) don’t have the same problem that Read(adj) and Read(v) do. Seen(adj) and See(v); Viewed(adj) and View(v). And so on. Then you can “translate” to English to actually use Unread/Read/[Read] if you still like it without confusing the translator who need to know you want more like e.g. Lido/Ler or 阅读/显示 and so on.

                                            2. 3

                                              Much better than the original article. Also love how many of the counter examples come from English.

                                            3. 16

                                              My bar for these lists is https://yourcalendricalfallacyis.com/ and most “falsehoods programmers believe” lists don’t meet it.

                                              1. 6

                                                The number of exceptions caused by the Hebrew calendar makes me shed a tear of joy.

                                                Here’s one falsehood they missed: the length of a year varies by at most one day. True in Gregorian calendar, apparently true in the Islamic calendar, but not true in the Hebrew calendar: leap years are 30 days longer than regular years.

                                                1. 2

                                                  They sorta cover it on the “days” section, by way of mentioning that the Hebrew calendar has leap months.

                                                  They also miss Byzantine calendars which are still used by many churches, related to the Jewish Greek calendar from the Septuagint. It’s of course complicated by the fact that many churches & groups do not agree on what year was the start, so it’s complex to use (but still in somewhat fairly common liturgical use).

                                                  1. 1

                                                    Wow 30? I need to red more about this

                                                2. 10

                                                  Here’s a fun (counter)example of (something like) this one from my heritage language:

                                                  In each language, the words for yes and no never change, regardless of which question they are answering.

                                                  (Context: the word for enjoy/like is the same in the language, so when translating to English, I choose whichever sounds most natural in each given example sentence.)

                                                  When someone says, “do you (enjoy/)like it?”, if you want to say “yes, I like it”, that’s fine, but if you want to say you don’t like it, you would say, “I don’t want it”; if you were to say, “I don’t like it” in that situation, it would mean, “I don’t want it”. The same reversal happens if they ask, “do you want it?”, and you want to respond in the negative.

                                                  So someone would say, “do you want a chocolate bar?”, and you’d say, “no, I don’t want it”, and that would mean, “no, (I already know) I don’t (usually/habitually) enjoy it (when I have it), (therefore I don’t want it)”, whereas, “no, I don’t enjoy it” would just straightforwardly mean, “I don’t want it”.

                                                  (You can also respond with just, “no!” instead of using a verb in the answer.)

                                                  This only happens in the habitual present form. Someone might ask, “do you like chocolate?” before they offer you some, and you can say, “no, I don’t want it”, but if they already gave you a chocolate bar to try, they may ask, “did you like it?” in the past tense, and you’d have to respond with, “I didn’t like it” instead of, “I didn’t want it”. And, “do you want chocolate?” would be met with, “no, I don’t like it”, but “did you want chocolate?” would be met with, “no, I didn’t want it”, and that second one would just mean what it straightforwardly sounds like in English.

                                                  (Strictly speaking, it doesn’t need to be a response to a question, I’m just putting it into a context to show that the verb used in the answer isn’t just a negative form of the same verb used in the question.)

                                                  (It’s hard to explain because if you were to translate this literalistically to English, it wouldn’t even be noticed, since saying, “no, I don’t like it” in response to, “do you want it?” is quite natural, but does literally just mean, “I don’t like it”, in the sense of, “no, (I already know) I don’t (usually/habitually) enjoy it (when I have it), (therefore I don’t want it)”. Even, “no, I don’t want it“ in response to, “do you like it?” is fairly natural in English, if a little presumptive-sounding.)

                                                  1. 4

                                                    In Polish when someone asks you “Czy chcesz cukru do kawy?” (“Do you want coffee with sugar?”) and you can respond with “Dziękuję”, which can mean 2 opposite things “Yes, please” or “No, thank you”.

                                                  2. 6

                                                    The original ones, like “…Names”, don’t; part of what I find fun about them is trying to think of counterexamples.

                                                    1. 6

                                                      I think if you want them to be useful they need to include counterexamples. If it’s just a vent post then it’s fine to leave them.

                                                      1. 4

                                                        The first one gets a pass because it was the first one, and even then, I think it’s better to link people one of the many explainers people wrote about it.

                                                    2. 13

                                                      This is an incredibly strange article. It has a few technical inaccuracies (Box IS a Sized type, the size of a pointer to an object doesn’t depend on the size of the object itself), but more fundamentally, it doesn’t actually answer the question it poses in the title.

                                                      For example, it complains that fat pointers are bad because they’re not supported by FFI. Nobody* writing a business app is going to care about FFI.

                                                      The rest of the article is full of “standard” complaints about Rust that I think have been fairly widely debunked, or just represent a not-well-informed view of things (e.g., the borrow checker is too hard, async sucks, etc) but even if true none of these criticisms are specific to business apps, it’s just a critique of the language itself.

                                                      I also just had to laugh at this bit:

                                                      While languages such as Go enable you to write pretty much entire HTTP service from standard lib alone, this bazaar-style package management comes with the burden: whenever you need to solve any mundane problem, you land in a space where everything has at least 7 different crates available, but half of them are actually a toy projects and most of them were not maintained for the last 5 years. And don’t get me started about audits to check if one of 600 dependencies of your hello world app won’t be used for supply chain attacks.

                                                      Yes, dependency management is a concern, but comparing to Go which famously refuses to implement basic features in the language, and then expects you to import xXx69roflcopterxXx’s github repo which is widely accepted as the best library in the ecosystem is a bit hilarious to me.

                                                      • Yes, yes, I’m sure that somebody somewhere has tried to write a business app with FFI included, but it’s certainly not the norm.
                                                      1. 4

                                                        Yes, dependency management is a concern, but comparing to Go which famously refuses to implement basic features in the language, and then expects you to import xXx69roflcopterxXx’s github repo which is widely accepted as the best library in the ecosystem is a bit hilarious to me.

                                                        Rust is the exact same, it just hides the GitHub repo names better. The Cargo package json would in Go be spelled "github.com/maciejhirsz/json-rust".

                                                        Well, not the exact same, because if you also decide to write your own json package for Rust you can’t register it in crates.io due to its no-namespaces naming policy. Go doesn’t restrict how many packages are allowed to match the pattern "github.com/*/json".

                                                        1. 2

                                                          Nobody* writing a business app is going to care about FFI.

                                                          I’m not sure what you call a business app. All the ones I know always have some dll-based runtime plugin / extension mechanism.

                                                        2. 30

                                                          I’d say this needs to s/No/Not yet/ its answer.

                                                          On the other side, the std lib itself is woefully lacking: no RNG, cryptography or serialization.

                                                          You’d prefer we either be frozen with the 0.1 version of the RNG API and rustc_serialize instead of Serde or have a Python/Java-esque situation where everyone tells you to ignore the standard library implementation and use a de facto-standard successor instead. (eg. Requests for Python and its internal never-will-be-in-stdlib urllib3, with basically any other networking code in the standard library being “use Twisted instead” for ages.)

                                                          Rust’s development philosophy was informed by the saying in the Python world that the standard library is where modules go to die, because the API stability requirements strangle further development. That’s why crates like rand are effectively “pieces of the standard library distributed through cargo”… because distributing through Cargo allows you to use older libraries with newer toolchains and vice versa.

                                                          Every time I see this argument, it comes across as people seeming to believe that you can force a simple solution to a complex problem.

                                                          Even some things that should have been language features since day one - like async traits and yield generators - are supplied as 3rd party macros.

                                                          Except that, if they chose to do that, it wouldn’t mean they arrived sooner… it’d mean Day One is still in the future.

                                                          Rust follows a “release a minimum viable product and extend it later” model and they already regret the things they were over-eager to implement and have to support in deprecated form in perpetuity, like the description and cause methods on the Error trait.

                                                          Async traits are in the process of being implemented. Generators exist in a “not yet safe for unconstrained use” form as the underlayer of async/await and are in the process of being implemented more generally.

                                                          Not abstract enough

                                                          Discussion is ongoing on ways to improve this. For example, one idea is keyword generics, which could allow functions to be generic over whether they’re being called in an async context. (The idea of abstracting over & vs. &mut hasn’t seen as much progress because it tends to get into the weeds over whether it’s actually a good idea, given the underlying semantic difference between them and how likely it is to actually need different code to achieve correctness.)

                                                          However Rust also decided to turn Box/Arc into fat pointers (similar to Go, and opposite to Java/.NET).

                                                          Your first two points sound like “I believe the “dyn Trait wouldn’t be a fat pointer”-ness of C++-style classes with internal vtables has benefits so valuable to things like C FFI and lock-free compare-and-swap that they should be offered as an option in the language itself”, which I can see reasonable discussion around. However, your third point is talking about generics being limited to Sized things… which is hard to argue as anything other than faulting Rust for not universally using C++-style classical inheritance for polymorphism since, if it’s anything but the only option, you’re back to some things satisfying the constraint and some not.

                                                          Rust provides a workaround in form of dedicated crates that offer thin dynamic pointers as well, but since they are not part of standard lib, it’s unlikely that you’ll be able to use them across different libraries in the ecosystem without extra work.

                                                          The general answer for situations like this is that, as with what happened with lazy_static → once_cell → std::sync::Once*, the Rust upstream are waiting to see if anything emerges in the ecosystem which is used ubiquitously enough to justify bringing it in.

                                                          (Rust has a philosophy that, if it can be prototyped, iterated on, and prove its desirability as a macro first, it should. Heck, ? began as try! even though it was part of the standard library in both forms.)

                                                          For example, delegate is used in two to three orders of magnitude fewer crates than once_cell and lazy_static, depending on how much overlap there is between them, and there’s the added barrier to getting implementation inheritance in the standard library that there’s no single way to do it that’s unarguably best. Each one has trade-offs and none has been excessively more desirable than the others so far.

                                                          I get why it’s there, but forcing it blindly and everywhere as a default behaviour is fucking bullshit: which apparently is acknowledged by the authors themselves, since the common way of getting immutable/mutable reference from an array is to split it into two objects using method that operates using unsafe pointers under the hood. Shutout to all haters saying that unsafe Rust is not idiomatic: it’s not only idiomatic, it’s necessary.

                                                          Honestly, this section’s take on the borrow checker feels not unlike calling math bullshit because Gödel’s incompleteness theorems exist.

                                                          I’m reminded Monty from xiph.org explaining that Gibbs Effect ripples are “just part of what a band-limited signal is”.

                                                          Even if Rust didn’t have to deal with being suitable for niches where you can’t bring your own tracing garbage collector, those Share-XOR-Mutate semantics are necessary to make ensuring correctness of multi-threaded code at compile time tractable.

                                                          Another thing about borrow checker is that it has very shallow understanding of your code. It also explicitly makes a conservative assumption that if you call method over some reference, this method will try to access ALL fields of that references, forcing any other field accessed outside of it to be invalidated.

                                                          Partial borrows are another topic of ongoing discussion. (Basically, given limited developer bandwidth, they’ve been put low on the priority list compared to things like the work to get async as close to the level of ergonomics as sync enjoys as possible.)

                                                          In fact, one of the things that’s been prioritized higher than partial borrows is the Polonius project to rewrite the borrow checker to finally be able to address Non-Lexical Lifetimes Problem Case #3. (i.e. making the borrow checker smarter)

                                                          Performance ceiling vs time to performance

                                                          Fair… though, for me, what matters is that Rust is much more likely to throw up a compiler error if I blindly try to do something which would have me fall off the fast path, while GCed languages will just happily pessimize performance to avoid distracting me from thinking about my business logic.

                                                          I’ve always seen Rust as less about performance and more about predictability.

                                                          …though…

                                                          Time to performance which basically means: “how long it takes to solve a problem with acceptable performance”. And personally: Rust falls behind many managed languages on that metric, mainly because of things like borrow checker, and multi-threading issues etc. which I cover later.

                                                          Given that I see Rust in the context of multi-threading to be more about correctness, I’m reminded of this quote:

                                                          “Why are you in such a hurry for your wrong answers anyway?” – Attributed to Edgser Dijkstra

                                                          So once you finally get your PhD from lock algorithms in Rust, you finally are at the level where you can do the job as efficiently as Go dev after learning the whole language in 2 hours.

                                                          You are aware, I hope, that Go isn’t memory-safe in the presence of data races. IIRC, it has to do with passing around certain kinds of multi-word values without synchronization in ways that can invoke undefined behaviour.

                                                          Panics

                                                          I fully agree. The lack of a maintained tool in the vein of Rustig or findpanics is embarrassing and, along with LLVM not providing the information necessary to turn “this optimization used to get applied and now it doesn’t” into a compile-time error, is one of the biggest things I dislike about Rust.

                                                          1. 7

                                                            Async traits are in the process of being implemented.

                                                            There’s one thing about async traits that I’m of two minds of: I think that we could have stabilized async fn in traits by de sugaring to Box<dyn Future> ages ago, with the object safety restrictions and (tiny) perf hit that comes from that, to provide value to people earlier while we still worked on impl trait in trait, but fear that if we had done that the pressure to ship impl trait in trait would have deflated and that project would never have seen the light of day, causing the “imperfect and temporary” solution to stick around.

                                                            Generators exist in a “not yet safe for unconstrained use” form as the underlayer of async/await and are in the process of being implemented more generally.

                                                            The issue with generators where that there are 3 related things (in order of blocking stabilization more to less):

                                                            • the traits involved, including Stream/AsyncIterator that have been under discussion for years
                                                            • the decision on how desugaring of async generators should be (and how to deal with pinning rules specifically, if you have to always pin the whole stream manually which is more flexible but more annoying or not which is easier to start doing but more restrictive)
                                                            • design around whether to provide special syntax to iterate on streams (think for await f in stream). You don’t need this because you can use while let always, adding await to patterns is a can of worms (it can be made to work, also amusingly you could make it so let await x = y; and let x = y.await; were equivalent, but what you also want is some way of selecting parallel batch size, like rayon’s par_iter as well)

                                                            Fair… though, for me, what matters is that Rust is much more likely to throw up a compiler error if I blindly try to do something which would have me fall off the fast path, while GCed languages will just happily pessimize performance to avoid distracting me from thinking about my business logic.

                                                            I would like it if we could supplement this gap more with Rudy’s lint’s: make the easy code fast, make the code do what the user expects (“I’m returning a boxed a trait here, so method access are through a v-table”) but detect the cases where an optimizing compiler would switch strategies and tell the user how to write it differently (“this function is used in a hot loop, and only ever returns one of two types, use impl Trait instead”). This keeps the developer in control, choices are explicit, and mild changes in the code don’t have sudden hidden performance cliffs because of changes from the assumption. Basically, every rule where Swift would change its code gen strategy should be a lint.

                                                            1. 4

                                                              I think that we could have stabilized async fn in traits by de sugaring to Box<dyn Future> ages ago,

                                                              This would have made async traits depend on a memory allocator, which would rule out its use from many interesting use cases (e.g. embedded systems).

                                                              1. 1

                                                                Yes, but changing the underlying mechanism to impl Trait was a backwards compatible change. I called out that the trade off would have been potentially delaying benefits for a few but enable a lot of people earlier.

                                                                1. 4

                                                                  Yes, but changing the underlying mechanism to impl Trait was a backwards compatible change.

                                                                  I don’t see how that could be true? If the feature is implemented by desugaring to Box then trying to remove that desugaring is also a backwards-incompatible change. For example, assuming an implementation similar to https://github.com/dtolnay/async-trait, you might have:

                                                                  let foo = some_async_trait_obj.some_method();
                                                                  let foo_ptr = Box::leak(foo.into_inner());
                                                                  

                                                                  Hiding memory allocations in normal-looking code also goes completely against the Rust use case, how are you going to verify that a particular code path has no allocations in it if any trait object’s method call might potentially be a hidden Box::new() ?

                                                            2. 3

                                                              That’s why crates like rand are effectively “pieces of the standard library distributed through cargo”…

                                                              Interesting you use rand as an example, because it seems like the opposite situation in play – the crates.io rand library isn’t maintained by the Rust developers, it’s just under some random GitHub org at https://github.com/rust-random/rand.

                                                              There are some crates.io packages that are like extended parts of the stdlib (flate2, git2, getopts) but they’re not necessarily the most popular solution in their space. It’s also difficult to discover which packages are under rust-lang/ and which aren’t without digging through the crates.io index metadata.

                                                              In contrast, Go has a larger built-in stdlib than Rust, but also has an extended stdlib distributed as regular packages. golang.org/x/crypto, golang.org/x/text, and golang.org/x/net/http2 are all maintained by the Go developers – if you want a SHA2-256 checksum then the stdlib has you covered, the more esoteric BLAKE2b is in golang.org/x/crypto.

                                                              In Rust if you use the crates.io sha256 package then you’re depending on https://github.com/baoyachi/sha256-rs, which as of the most recent version (v1.6.0, released 3 days ago) doesn’t even have no_std support.

                                                              1. 8

                                                                I certainly agree that we need better tooling to indicate which things are officially “external pieces of the standard library”. However, by Python 2.7, half of Python’s standard library was under de facto deprecation in favour of libraries with no connection to the stdlib dev team, with the identity of those packages being communicated through tribal knowledge.

                                                                Putting stuff in Rust’s actual standard library wouldn’t prevent the “Batteries Included, But They’re Leaking” effect and there’s only so much that “this is officially maintained” can do to put a thumb on the “who’s competing more effectively?” scale.

                                                                Again, this comes back to “Go’s standard library is what it is because Google is paying for the manpower, not because declaring things std magically summons more contributors”.

                                                                Having seen the state Python was in, the philosophy behind Rust’s standard library is that it’s for two kinds of things:

                                                                1. Interface types that need to be in core or std for language constructs to depend on. (Which is why Result and Future are in std for ? and async to depend on, but the http crate is the de facto standard source of HTTP request and response interface definitions and is external.)
                                                                2. Things that are so universal that they wind up in every non-trivial project and which have stabilized on an agreed-upon solution. (eg. Every non-trivial project includes once_cell, lazy_static, or both, with it being agreed that the younger once_cell is superior for use in new code. Hence the work to incorporate a version of once_cell into std as std::sync::Once*.)

                                                                That’s also why, once they finish getting async traits implemented, they intend to work on adding traits that’ll finally allow people to stop specifically writing support for each async executor they want to be compatible with.

                                                                A lot of people are also unaware that std has replaced the innards of the standard library HashMap, Mutex, and std::sync::mpsc with more performant third-party crates since v1.0 (hashbrown, parking_lot, and crossbeam_channel, respectively) but you may still want to use them directly to access features precluded by needing to preserve the std APIs externally, such as hashbrown’s no_std support, upstream parking_lot’s non-poisoning mutex API, or crossbeam_channel’s support for MPMC queueing. (I’m not 100% certain, but I believe hashbrown is being depended on while parking_lot and crossbeam_channel are being vendored and possibly patched.)

                                                                1. 2

                                                                  My opinion on the Python standard library is that its problems are not caused by things being in the standard library, they’re caused by Python’s habit of shipping half-baked functionality in its stable releases. Nobody complains about the Python stdlib packages that through design or luck were working from the beginning.

                                                                  Rust has the stable/beta/nightly system, which means things like sha256 could hang out in nightly for a while and get its API tweaked according to real-world usage before being stabilized. Python just … ships things, in whatever form they’re in on release day, and relies on its dynamic type system to let fixes get shimmed in later.

                                                                  Again, this comes back to “Go’s standard library is what it is because Google is paying for the manpower, not because declaring things std magically summons more contributors”.

                                                                  I disagree that Rust’s small standard library and/or small extended standard library is due to a lack of contributors. IMO it’s due to the libs-team putting themselves in the blocking path of every new library feature, but refusing to expand libs-team large enough to handle the review load.

                                                                  Any reasonably capable Rust developer could write and submit a competent Rust equivalent to Go’s encoding/base64 or hash/crc32, but if the ACP review and RFC review take multiple years to proceed (with uncertain chance of success) then what’s the point?

                                                                  If money is the concern, you could imagine the libs-team having a trusted set of “available for hire” folks who are available to do a preliminary review. Someone who wants a feature could pay $5000 or whatever to make sure the ACP and/or RFC are good enough for the libs-team, then take that proposal through a fast-track review process.

                                                                  But there’s no such mechanism, it’s all just slow-grinding bureaucracy by people who simultanously complain about the workload but also restrict their own team capacity to the point that it can barely keep up with maintenance.

                                                                  1. 9

                                                                    We already have deprecated methods on things like the Error trait which took half a decade to be realized to be the wrong implementation, and that’s with most of the churn being farmed out to error-chain → failure → anyhow/this-error (with some people using a form of anyhow named eyre) while fehler, written by one of the Rust team, languished and got retired because I’m apparently not alone in not wanting more exception-like syntax in Rust error-handling, and other error-handling things like snafu still manage to carve out very non-trivial niches despite “anyhow+this-error, oh, actually, eyre is better than anyhow” being what everyone gets pushed toward by the common wisdom.

                                                                    It’s not a function of them being half-baked. It’s a function of the reality that it can take 5 or 10 years (or more) to realize that an idea isn’t ideal. That’s why C++ has classical inheritance baked into its core while Rust doesn’t, or why C++ has three different APIs for things like retrieving members from a vector with the nicest, most concise one being the most hazardous to use because it came first. It’s why APIs that remain available from 90s-era Windows NT contain a frozen snapshot of every piece of 90s common wisdom about good software design except for the part where they realized early on that they couldn’t make a microkernel performant enough. (eg. the deprecated fibers APIs and the “APIs, not escape sequences” design of the Win32 console that they finally turned into a GNU Screen-esque compatibility proxy with the advent of Windows Terminal since SSH has become the dominant way to use consoles.)

                                                                    The Rust devs are optimizing for the reality that all languages will accumulate legacy baggage by trying to strike a balance which allows as much of the language as possible to be swapped out without turning it into Forth.

                                                                    1. 1

                                                                      We already have deprecated methods on things like the Error trait which took half a decade to be realized to be the wrong implementation,

                                                                      Were they the wrong implementation, though? Sure, they’re deprecated now, but they still work for both old and new code. It’s natural that not all new functionality is completely disjoint from old functionality – sometimes the old stuff gets replaced by a better solution. Also by my calendar it only took three years (Rust v1.0 in 2015, Error::source() added in v1.30 in 2018), but that’s a minor quibble.

                                                                      Regarding the specific example of Error, my only gripe about its evolution is how long it took to become available from core. I actually ended up commenting on the stabilization issue[0], and maybe had some small influence on core::error::Error getting unwedged from the whole backtrace / provider thing.

                                                                      [0] https://github.com/rust-lang/rust/issues/103765#issuecomment-1801053112

                                                                      and that’s with most of the churn being farmed out to error-chain → failure → anyhow/this-error […]

                                                                      I might be missing context here, but what do those packages have to do with the standard library? As far as I know none of the Rust stdlib’s error handling logic was imported from a third-party package.

                                                                      It’s natural that a design space as wide and varied as “generic handling with arbitrary nesting of dynamically-allocated error values” will have a lot of ideas on how to implement it, but the Rust stdlib can’t use those designs anyway, so from the standpoint of trying to expand the stdlib all that churn … kinda doesn’t matter? Like, who actually cares about anyhow vs this-error or whatever? Just be careful not to expose them in your library’s public API and pretty much anything will work well enough.

                                                                      It’s not a function of them being half-baked. It’s a function of the reality that it can take 5 or 10 years (or more) to realize that an idea isn’t ideal.

                                                                      This may be a difference of opinion, but I don’t think it’s important that the standard library contain only things that are “ideal”. 5-10 years is an extraordinarily long time for a stdlib feature to be unstable – by that time all of the potential users have already implemented it themselves and moved on.

                                                                      That’s why C++ has classical inheritance baked into its core while Rust doesn’t, or why C++ has three different APIs for things like retrieving members from a vector with the nicest, most concise one being the most hazardous to use because it came first.

                                                                      But it’s not the lack of design time that caused C++ to be like that! C++ is designed the way it is because that’s what the designers of C++ want it to be! That’s like saying that Rust shouldn’t have stabilized until it had found a way to eliminate the borrow checker and &str / &[u8] distinction.

                                                                      The Rust devs are optimizing for the reality that all languages will accumulate legacy baggage by trying to strike a balance which allows as much of the language as possible to be swapped out without turning it into Forth.

                                                                      I understand that’s what they’re trying to do, my position is that they should stop trying to do that.

                                                                      There is clearly a middle ground between Python and Go – which allow silly nonsense like XML or HTTP stdlib modules, despite XML being way too complex for a stdlib and HTTP being an evolving standard – and the abject minimalism of Lua or Scheme.

                                                                      1. 6

                                                                        I might be missing context here, but what do those packages have to do with the standard library? As far as I know none of the Rust stdlib’s error handling logic was imported from a third-party package.

                                                                        The “native” Error handling experience is something people have been complaining about often since day one, but the Rust standard library is what it is because all the churn was kept external to it. That’s the relevance.

                                                                        This may be a difference of opinion, but I don’t think it’s important that the standard library contain only things that are “ideal”.

                                                                        Then you’re at odds with the core precept that the Rust devs abide by. Since the Rust v1.0 Stability Promise, they want to minimize how much sub-optimal “use this third-party thing instead” cruft piles up in the standard library.

                                                                        …given how much they hew to that tenet, discussing the difference of opinion is only relevant if you are trying to decide whether to start a fork.

                                                                        5-10 years is an extraordinarily long time for a stdlib feature to be unstable – by that time all of the potential users have already implemented it themselves and moved on.

                                                                        …and they’re fine with that, as demonstrated with their approach to lazy_static, once_cell, and std::sync::Once* and their unconcernedness that it’s taking so long for a clear and popular winner to emerge on the question of implementation inheritance. (i.e. “Your language doesn’t need to have everything. If it’s that hard to find a solution that meets the standard, maybe the best answer is to do nothing”.)

                                                                        But it’s not the lack of design time that caused C++ to be like that! C++ is designed the way it is because that’s what the designers of C++ want it to be!

                                                                        True. As is every language. I’m saying that what the Rust designers want is for Cargo to be the complement to the editions system, given that editions can’t remove stuff from the standard library.

                                                                        That’s like saying that Rust shouldn’t have stabilized until it had found a way to eliminate the borrow checker and &str / &[u8] distinction.

                                                                        I disagree. In fact, I think that the ignorance of the theoretical underpinnings of those constructs that you’re demonstrating is so high that it becomes “not sure if arguing in good faith”.

                                                                        First, because the borrow checker is just as much about ensuring the correctness of multi-threaded code in the presence of mutable state as it is about managing memory, and the difficulty of preserving that property while eliminating it is “cutting edge of theoretical research”-level difficult, not “let’s just find a sufficiently comfortable design for this API” difficult. (i.e. That comparison, to me, comes across as the same class of misunderstanding as “Mathematicians could give division by zero a defined behaviour tomorrow if they wanted to.” …if you give division by zero a defined behaviour, you can use it to write proofs for things like 1 = 2.)

                                                                        Second, because the &str / &[u8] distinction isn’t a technical limitation. It’s an intentional decision to enforce invariants at the level of the type system which would have been more clear if they’d been named ValidUtf8String and ByteString instead. Basically, implying that it’s a problem to be solved, rather than a solution to a problem, is akin to implying that C’s refusal to compile a goto into the middle of some other function is a problem to be solved, rather than a solution to a problem. (specifically, an explicit decision to enforce structured programming principles at the language level.)

                                                                        There is clearly a middle ground between Python and Go – which allow silly nonsense like XML or HTTP stdlib modules, despite XML being way too complex for a stdlib and HTTP being an evolving standard – and the abject minimalism of Lua or Scheme.

                                                                        Rust is hardly Lua or Scheme. Personally, I think that they’re striking a fairly good balance, aside from the lack of an official analogue to https://blessed.rs/

                                                                        1. 2

                                                                          The “native” Error handling experience is something people have been complaining about often since day one,

                                                                          Who and why? As far as I can tell the native error handling is pretty much perfect, since it allows rich error information to be propagated up the stack with very little overhead. A struct mypkg::Error { code: u32 } fits in a single register, and even a pretty complex enum mypkg::RichError<'a> { Something(&'a SomethingDetails), ... } will get a nice compact representation that’s equivalent to a manually-written union.

                                                                          Conversely, libraries like anyhow require a heap allocation for each error value, and it’s pretty difficult to figure out what types an anyhow::Error actually contains. Instead of returning Result<Something, SomeError> the code returns Result<..., anyhow::Error> and the actual error type can vary at runtime.

                                                                          I disagree. In fact, I think that the ignorance of the theoretical underpinnings of those constructs that you’re demonstrating is so high that it becomes “not sure if arguing in good faith”. […]

                                                                          You misunderstand my point.

                                                                          The distinction between &str and &[u8] is important to the design or Rust for all the reasons you mention. My point is that the current C++ design (including operator[] being the most ergonomic and also most unsafe way to access a std::vector) is what the designers of C++ want it to be. I don’t think the C++ committee considers operator[] to be a failure of the design process, and if they’d had an extra 5 years to ponder it I think they would have produced the same outcome.

                                                                          When you say “It’s a function of the reality that it can take 5 or 10 years (or more) to realize that an idea isn’t ideal. That’s why C++ has classical inheritance baked into its core while Rust doesn’t,” it reads to me as if you think the designers of C++ would have implemented a different design if they’d spent longer thinking about the problem, which I don’t agree with.

                                                                          Rust is hardly Lua or Scheme. Personally, I think that they’re striking a fairly good balance, aside from the lack of an official analogue to https://blessed.rs/

                                                                          Rust’s approach to the standard library is much closer to that of Lua or Scheme than it is to Python or Go, in ways that I think cause adoption of Rust to be slower than it could otherwise be given a richer stdlib.

                                                                          Regarding sites like https://blessed.rs, I don’t think they’re useful. The crates.io download statistics do well enough for people who just want a catalog of popular libraries, and that page has no analysis or benchmarks or code audit reports or anything that would help guide someone who’s trying to figure out which Rust libraries are good enough to use as-is.

                                                                          A page like https://asomers.github.io/mock_shootout/ (comparison of mocking libs for Rust) or https://github.com/MartinThoma/algorithms/tree/master/Python/json-benchmark (JSON libraries for Python) would be much more useful, since it provides at least some basis for comparison.

                                                                          1. 2

                                                                            Who and why?

                                                                            Generally anyone who hasn’t resolved to use either anyhow and/or thiserror-like libraries or Box<dyn Error> and sees it as a flaw in the language that you need a third-party crate to avoid writing “all that boilerplate”… so basically every newcomer and anyone who is opinionated enough to stick to their guns that it’s a flaw to need third-party crates to define custom error types less concisely than class MyError(Exception): pass

                                                                            …especially when, from what I remember, Box<dyn Error> only became viable a while into Rust’s lifespan. (And I’m not just talking about the syntax change for how you name a trait object.)

                                                                            (These days, you tend to see those people faulting Rust for not having structural types so you could do something like fn foo(bar: Bax) -> Result<Quux, ErrA | ErrB>)

                                                                            My point is that the current C++ design (including operator[] being the most ergonomic and also most unsafe way to access a std::vector) is what the designers of C++ want it to be. I don’t think the C++ committee considers operator[] to be a failure of the design process, and if they’d had an extra 5 years to ponder it I think they would have produced the same outcome.

                                                                            I dunno. On the one hand, from what I’ve read about them, the C++ committee do seem to have a stubborn old man “In my day, we didn’t have no seatbelts or crumple zones. That’s a skill issue.” mindset toward what a programming language should be, even to this day but, on the other hand, C++ was trying to be state of the art for “C with Classes” so I could see them deciding differently before they became so emotionally invested in their current design.

                                                                            Rust’s approach to the standard library is much closer to that of Lua or Scheme than it is to Python or Go, in ways that I think cause adoption of Rust to be slower than it could otherwise be given a richer stdlib.

                                                                            I see it as a “the candle that burns twice as hot burns half as long” situation. Yes, it could help Rust to get uptake faster, but it’ll sabotage its longevity in the face of inevitable changing surroundings.

                                                                            Regarding sites like […]

                                                                            Fair. Let’s say a site like https://djangopackages.org/ and its grids instead.

                                                                            1. 2

                                                                              every newcomer and anyone who is opinionated enough to stick to their guns that it’s a flaw to need third-party crates to define custom error types less concisely than class MyError(Exception): pass

                                                                              Well, the whole challenge with going from a super high-level scripting language like Python to a low-level systems language like Rust is that things are going to get more verbose. That’s just a natural consequence of systems programming requiring more precise control over compiler output and runtime behavior.

                                                                              Sometimes I get the feeling that people coming from Python/Ruby/JavaScript to Rust would have been happier if they instead went to Java or C# (or Kotlin / Scala / F#), but they avoided those languages for non-technical reasons. There’s nothing wrong with requiring an allocation per error value if your language also has a garbage collector and doesn’t distinguish the stack from the heap in type signatures.

                                                                              (These days, you tend to see those people faulting Rust for not having structural types so you could do something like fn foo(bar: Bax) -> Result<Quux, ErrA | ErrB>)

                                                                              Yeah, that’s definitely a high-level way of thinking about it. Structural type systems are interesting for interfaces because they allow looser coupling between libraries (I like Go’s interface, and IIRC Swift does something similar) but it’s difficult to say what role they’d serve as concrete types in Rust.

                                                                              What kind of ABI would Result<_, ErrA | ErrB> even have? Presumably an implicit enum, but then matching on it becomes tricky (would Rust need Go-style type switches?) and the API evolution into an actual named enum is unclear.

                                                                              Fair. Let’s say a site like https://djangopackages.org/ and its grids instead.

                                                                              That seems to have a related problem, it’s got a comparison but the package selection is way too un-curated.

                                                                              I used Django ~15 years ago in my first job out of university, and one of the packages I uploaded to PyPI at that time was django-genshi. It’s not been updated since, and yet when I go to https://djangopackages.org/grids/g/template-engine-adapters/ it’s listed!

                                                                              Nobody should be using that package! Nobody with any common sense would include that package in a contemporary list of high-quality Django-related packages!

                                                                              It reminds me of those autogenerated blogspam articles that pop up in Google results when one searches “best oven degreaser” or whatever.

                                                                              1. 3

                                                                                Sometimes I get the feeling that people coming from Python/Ruby/JavaScript to Rust would have been happier if they instead went to Java or C# (or Kotlin / Scala / F#), but they avoided those languages for non-technical reasons.

                                                                                “You can’t feasibly write in-process loadable modules for CPython using Java or C# (or Kotlin / Scala / F#)” is a non-technical reason?

                                                                                (Seriously. My big reason for using Rust is that I can write modules that can then be shared between my PyQt, Django, WebAssembly, etc. projects as well as used in fast-starting, statically-linked CLI tools.)

                                                                                Yeah, that’s definitely a high-level way of thinking about it. Structural type systems are interesting for interfaces because they allow looser coupling between libraries (I like Go’s interface, and IIRC Swift does something similar) but it’s difficult to say what role they’d serve as concrete types in Rust.

                                                                                What kind of ABI would Result<_, ErrA | ErrB> even have? Presumably an implicit enum, but then matching on it becomes tricky (would Rust need Go-style type switches?) and the API evolution into an actual named enum is unclear.

                                                                                nod One of the big oppositions listed in RFC discussions is that the risk of a pile of type aliases containing a semantically significant A | A after a semver-compatible update to a transitive dependency is about as high as allowing type inference in function signatures.

                                                                                Nobody should be using that package! Nobody with any common sense would include that package in a contemporary list of high-quality Django-related packages!

                                                                                …and that’s surfaced in rows like “Last Updated”. I’m assuming it’s included in case nothing else meets your needs and you want to take up maintaining a fork for your project as has happened with things like tikv-jemallocator in Rust. (Now the successor, with the original crate being updated as a duplicate of it for compatibility.)

                                                                                1. 2

                                                                                  “You can’t feasibly write in-process loadable modules for CPython using Java or C# (or Kotlin / Scala / F#)” is a non-technical reason?

                                                                                  “I want to write business logic in a statically-typed compiled language with automatic memory management and a rich library ecosystem but don’t want to use JVM or .NET languages” would be an example of a non-technical reason.

                                                                                  If you want to be able to declare new error types with Python syntax, write your library in Python. If you want to write a CPython extension module, use PyErr_NewExceptionWithDoc and accept that CPython extension modules written in C (or Rust) have different ergononomics than Python.

                                                                                  (Seriously. My big reason for using Rust is that I can write modules that can then be shared between my PyQt, Django, WebAssembly, etc. projects as well as used in fast-starting, statically-linked CLI tools.)

                                                                                  If you want to write code in a language that is designed for systems programming then you’ll have to accept the tradeoffs that come with that. One of those tradeoffs is that the needs of people using it for microcontroller firmware or OS kernels will have a strong influence on the language design, including in the idiomatic way to represent errors.

                                                                                  In languages like Python or Java, errors contain a great deal of information about their underlying cause – formatted messages, stack traces, wrapped errors from further down the call stack. All of that extra detail requires dynamic memory allocation, which means it can’t be the standard error mechanism in a systems language like Rust. And the size of error values affects how quickly those functions can get called: an error code that fits into a register can be returned in %rbx, a rich error struct that takes up 64 bytes requires extra stack manipulation overhead on every call.

                                                                                  The Cambrian explosion you describe in third-party exception-ish libraries for Rust is a symptom of people using a language that is inappropriate for the level of abstraction they want to write code in, not a sign that the Rust standard library needs a new approach to error handling.

                                                                                  1. 4

                                                                                    I never said the standard library needs a new approach to error handling. I said that the standard library is as clean as it is because they don’t rush to be batteries included in the face of “no single solution currently in use is head and shoulders above the others” situations.

                                                                    2. 6

                                                                      Rust has the stable/beta/nightly system, which means things like sha256 could hang out in nightly for a while and get its API tweaked according to real-world usage before being stabilized.

                                                                      How is that better than distributing that same code as crates until they are deemed stable enough for inclusion in the std? Doing what you propose would mean that people using stable Rust (a majority of developers) wouldn’t be able to use those APIs anyways.

                                                                      1. 6

                                                                        *nod* I prioritize stability (I came from Python for the compile-time correctness and the “fearless upgrades”-focused ecosystem) and apparently enough other people do too that they’re worried about not getting enough people to hammer on nightly features before they get stabilized.

                                                                        (Seeing the “nightly” badge on lib.rs is an automatic Ctrl+W for me. I’d sooner NIH something than make my projects incompatible with the stable-channel compiler.)

                                                                        “nightly” crates aside, stuff in crates is available for beta-test by all Rust users, which is why they do it that way when it’s an option.

                                                                        1. 2

                                                                          How is that better than distributing that same code as crates until they are deemed stable enough for inclusion in the std?

                                                                          Because “they are deemed stable enough for inclusion in the std” doesn’t happen.

                                                                          Look at base64 (https://github.com/marshallpierce/rust-base64) as an example. Its still on a v0.x version after 7 years of development, the API requires allocation (Engine::encode returns a String), and it doesn’t seem to be using SIMD at all.

                                                                          What’s the path to that crate getting into core? I’m having a hard time seeing one.

                                                                          On the other hand, an unstable core::encoding:base64 could start off as a Rust adaptation of the design principles in https://github.com/simdutf/simdutf, so it’s not starting from scratch. The API of a base64 codec is not large, so it would have a pretty fast path to stabilization (maybe 1-2 years at most). And once it’s stabilized, Rust would have a clear solution to the question of “how do I do base64 encoding in a no_std project without having to run c2rust on simdutf?”.

                                                                          Doing what you propose would mean that people using stable Rust (a majority of developers) wouldn’t be able to use those APIs anyways.

                                                                          That’s fine. They can continue to use whatever other solution they were already using, and once the std / core version stabilizes they can switch over to it (or not) on their own schedule. Or they can do something like the futures package and extract snapshots of the unstable stdlib code into a crates.io package.

                                                                2. 14

                                                                  I wonder how many filesystems can be safely removed without any real effect on anyone. There are FUSE libs for pretty much everything, and if not, they can be made quite easily. Since the real use case for support for ancient and exotic filesystems is recovering information from defunct drives, having them in the kernel doesn’t bring any benefit.

                                                                  1. 5

                                                                    I’d bet money that befs (BeOS), hpfs (OS/2), and efs (pre-XFS IRIX) are equally unused and could be replaced by userspace drivers without any complaints.

                                                                    1. 3

                                                                      I have actually used the befs support recently to copy files from Haiku to Linux on a dual-boot system where the Linux partition is encrypted with LUKS (and thus Haiku can’t mount it). A user-space implementation would have worked just fine though.

                                                                      1. 1

                                                                        I’m surprised that befs is still around, because even haiku doesn’t really use that one. There’s really not much practical use in this case.

                                                                        1. 2

                                                                          even haiku doesn’t really use that one

                                                                          How do you mean? Are you saying that Haiku uses some other filesystem (at last as far as I can tell BFS is the default), or that what Linux implements is a different form of the file system?

                                                                          1. 3

                                                                            My bad, I got confused with Redox which uses own thing. You’re right that Haiku used and still uses BFS.

                                                                    2. 8

                                                                      Hi all! I finally decided to write the monad tutorial of my dreams, using Rust and property-based testing as the vehicle to understand them.

                                                                      A lot of monad explanations focus on either the formalism, or an IMHO excessive level of casualness. While those have their place, I think they tend to distract from what I think is an important and profound design pattern. My explanation has:

                                                                      • no analogies to food items or anything else
                                                                      • no Haskell
                                                                      • no category theory

                                                                      Instead, this post talks about:

                                                                      • real-world problems
                                                                      • production-grade Rust
                                                                      • with performance numbers, including a very pretty log-log cdf!

                                                                      There’s also a small Rust program to accompany the post.

                                                                      1. 6

                                                                        Hi, it’s commendable that you set forth to bridge communities and languages with this post, but among other TCS/algebraic concepts monads tend to get variously conflated with their implementation in this or that concrete language. However you make some pretty strong claims in the post, and some downright inaccurate ones.

                                                                        The thing we care about is composition of computations that might have effects. As such bind/flatMap are generalizations of the pure function composition “dot” operator.

                                                                        This is the essence of monadic composition: powerful, unconstrained, and fundamentally unpredictable.

                                                                        Not really. Lists and Optional computations can be composed with monadic bind, but would you say composition of programs that return these values is unpredictable?

                                                                        Some of this misunderstanding is a historical accident; Haskell lets you talk about effect types that do not involve I/O, so it proved to be a good test bench for this concept.

                                                                        monadic composition is Turing-complete

                                                                        This is a property of the language, not of the composition operator.

                                                                        1. 7

                                                                          Thank you for the feedback! For context, I studied some category theory in graduate school and I can also talk about natural transformations and endofunctors, but I made a deliberate decision to avoid all of that. There are a large number of developers who would benefit from recognition of monadic patterns in their daily professional lives, but have been traumatized by overly technical monad tutorials – that’s the audience for my post. If you’re not in that audience, there are plenty of other monad tutorials for you :)

                                                                          Even in the appendix, I talk about the monad laws in terms of their application to strategies with the concrete Just and prop_flat_map. That’s a deliberate pedagogical decision too. I enjoy working in abstract domains, I just think it really helps to be motivated by practical concerns before going deeper into them.

                                                                          but among other TCS/algebraic concepts monads tend to get variously conflated with their implementation in this or that concrete language.

                                                                          This is absolutely true. In my post I was trying to focus on why monadic bind is of deep interest and not just a Haskell/FP curiosity — why working programmers should at least notice it whenever it pops up, no matter what environment or language they are in. (Speaking personally, in one of my first projects at Oxide, very far away from grad school, I had the opportunity to add monadic bind to a system, but deliberately chose not to do so based on this understanding.)

                                                                          I think of it as similar to group theory, where you can either introduce groups formally as a set with an attached operation that has certain properties, or motivate them through an “implementation” in terms of symmetries (maybe even through the specific example of solving the quintic). I personally prefer the latter because it gives me really good reasons for why I should care about them, and also helps explain things like why group theory is central to physics. In my experience teaching people, this preference is shared by most of them. There’s always time later to understand the most general concept.

                                                                          Not really. Lists and Optional computations can be composed with monadic bind, but would you say composition of programs that return these values is unpredictable?

                                                                          Absolutely, relative to fmap in each of the monads. I mention an example at the end with lists, where with bind you don’t know the size of the resulting list upfront – for Rust this has performance implications while collecting into a vector, since you can’t allocate the full capacity upfront (though that’s a tiny difference compared to the exponential behavior of test case shrinking). Even with optionals, fmap means a Some always stays a Some, while bind can turn a Some into a None. Relatively speaking that’s quite unpredictable.

                                                                          The extent to which monadic bind’s unpredictability matters is specific to each monad (it matters less for simpler monads like optionals, and more for strategies, futures and build systems.) But in all cases it is unpredictable relative to functor (or applicative functor) composition within that monad.

                                                                          This is a property of the language, not of the composition operator.

                                                                          This is true. What I was trying to say here is that in a typical Turing-complete environment, the Turing completeness means that introspecting the lambda is impossible in general. (And this is true in non-Turing-complete environments too – for example, in primitive recursive/bounded loop environments it’s still quite unpredictable.) I’ll try and reword this tomorrow.

                                                                          1. 5

                                                                            I appreciate the thoughtful approach. I learn much better starting with the specific and moving to the general than starting with the abstract.

                                                                            I wrapped up Georgia tech OMSCS and one of my favorite classes was Knowledge Based AI which focused on how humans learn things. (And the AI is classical and not LLMs). One core takeaway for me was the general/specific learning dichotomy.

                                                                            A really interesting learning approach was called “version spaces” which acted like bidirectional search by building general and specific info at the same time. Basically, people need both models to fully absorb a concept, but how individuals learn best varies.

                                                                            All that to say: thanks again, I think it takes a lot of work and effort to make something approachable and I appreciate your post.

                                                                            1. 4

                                                                              You may enjoy this post: https://terrytao.wordpress.com/career-advice/theres-more-to-mathematics-than-rigour-and-proofs/

                                                                              This three-staged learning process is very common, in my experience, and i feel like general/specific is similar to intuitive/rigorous.

                                                                            2. 2

                                                                              There are a large number of developers who would benefit from recognition of monadic patterns in their daily professional lives, but have been traumatized by overly technical monad tutorials

                                                                              When intersected with “Rust developer”, do you think that’s still a large group…? If someone finds monad tutorials to be “overly technical” then they’re never going to make it past JavaScript, much less discover systems programming languages like Rust.

                                                                              I’m one of the fools who once upon a time tried to use Haskell for systems programming, and of all of the Rust/C/C++ developers I’ve met, their primary difficulty with monads is that all the documentation was non-technical (i.e. prose) and did the normal academic math thing of using five different words to identify the same concept in different contexts.

                                                                              This article is a new way of writing a confusing explanation of monads, which is that it starts off by diving deep into an obscure testing strategy that’s pretty much only used by people really into functional programming, and then slowly works its way back into the shallow waters of monads, then dives right into a discussion on how function composition is Turing-complete[0] and how exemplar reduction in property-based testing can have unpredictable runtime performance. You’ve got, like, three different articles there!

                                                                              If you want someone who can’t spell “GHC” to understand monads, there’s only three types you need:

                                                                              • Lists (Rust’s Vec, C++ std::vector) with flat_map
                                                                              • Maybe (Option, std::optional or local equivalent) with and_then
                                                                              • Either (Result, std::expected or local equivalent) with and_then

                                                                              Don’t go off into the weeds about property testing, only teach one alien concept at a time. Those three types have super-simple implementations of (>>=), they’re universally familiar to everyone who’s not been frozen in Arctic sea ice since 1995, and it’s easy to go from ? to do-notation if you want to demystify the smug “monads are programmable semicolons” in-joke while you’re at it.

                                                                              Then, once your reader is comfortable with the concept of nested inline functions as a control flow primitive, you can link them to your follow-up article about the performance implications of monadic combinators or whatever.

                                                                              [0] The article acts as if this is a surprising and meaningful revelation, so I might be misunderstanding what’s actually being discussed, but when you say “monadic composition is Turing-complete” you mean something like “bind :: (a -> T b) -> T a -> T b is Turing-complete”, yes? I feel like among people who know what “Turing-complete” means, the knowledge of a Turing machine’s equivalence to function composition is well-known.

                                                                              1. 7

                                                                                When intersected with “Rust developer”, do you think that’s still a large group…? If someone finds monad tutorials to be “overly technical” then they’re never going to make it past JavaScript, much less discover systems programming languages like Rust.

                                                                                Yes, it’s a large group. The number of Rust people that come from Haskell or other FP languages is tiny.

                                                                                This article is a new way of writing a confusing explanation of monads, which is that it starts off by diving deep into an obscure testing strategy that’s pretty much only used by people really into functional programming

                                                                                Property-based testing is quite widely used by Rust projects! It’s not the most common way to test software, but it’s far from obscure. It’s also really effective at finding bugs in systems code. I haven’t done any professional work in FP languages, but I’ve used PBT extensively.

                                                                                I even picked a very systems-y example, writing a production-grade sort function that’s resilient to comparator misbehavior. This is the the kind of thing Rust developers enjoy.

                                                                                and then slowly works its way back into the shallow waters of monads, then dives right into a discussion on how function composition is Turing-complete[0] and how exemplar reduction in property-based testing can have unpredictable runtime performance. You’ve got, like, three different articles there!

                                                                                Yes, deliberately so. This kind of progressive complexity enhancement, with a tutorial for A also being a tutorial for B in disguise, is the style I teach in. It’s one I’ve practiced and refined over a decade. It doesn’t work for everyone (what does?) but it reaches a lot of people who bounce off other explanations.

                                                                                If you want someone who can’t spell “GHC” to understand monads, there’s only three types you need

                                                                                With respect, I quite emphatically disagree. I personally believe this approach is a colossal mistake, and I’m far from the only one to believe this (I’ve had a number of offline conversations about this in the past, and after I published this post a professor reached out to me privately about this as well.) I deliberately picked PBT as one of the simplest examples of monadic bind being weird and fascinating.

                                                                                1. 4

                                                                                  With respect, I quite emphatically disagree. I personally believe this approach is a colossal mistake, and I’m far from the only one to believe this (I’ve had a number of offline conversations about this in the past, and after I published this post a professor reached out to me privately about this as well.) I deliberately picked PBT as one of the simplest examples of monadic bind being weird and fascinating.

                                                                                  The problem with only using List, Option and (perhaps to a lesser extent) Either, as examples is that they’re “containers” (List is commonly understood; Option can be understood as “a list with length < 2”; Either can be understood as Option whose “empty” case provides a “reason” (e.g. an error message)). Containers come with all sorts of intuitions that make interfaces like Monad less appealing. For example, what’s the point of y = x.flat_map(f) compared to ordinary, everyday, first-order code like for (element : x) { y += f(element); }?[0]

                                                                                  List (and Option) are definitely good anchors for our understanding, e.g. as sanity checks when reading a generic type signature or a commutative diagram; but we also need examples that aren’t “containers”, to show why these interfaces aren’t just some weird alternative to “normal” looping. A set of examples which aren’t “containers”, but may still be familiar, are things which “generate” values; e.g. parsers, random generators, IO, etc.[1]. Those are situations where our intuition probably isn’t “just loop”[2], so other interfaces can get a chance[3]. The fact that Monad & friends apply to both “containers” and “generators” is then a nice justification for their existence. Once we’re happy that this is a reasonable interface, we can go further into the weeds by deriving some examples purely from the interface, to get a better feel for what it does/doesn’t say, in general (e.g. a “container” that’s always empty, i.e. a parameterised unit type; or a Delay type, which captures general recursion; etc.).

                                                                                  [0] Indeed, Scala’s for is syntactic sugar for monadic operations, similar to Haskell’s do. Although that does require extra concepts like yield and <-, which don’t appear in e.g. a Java for-loop; and may require understanding of monad-like-things (I can’t say for sure, since I first tried Scala after after many years of Haskell programming!).

                                                                                  [1] Parsers and random generators are actually very closely related, e.g. we can think of a random generator as a parser that’s been given a random (but valid) input. Parser combinators are the most obvious way to understand what it means for parsers to be monads: they used to be quite academic, but I think are now common enough to be a motivating example for “why care”, even for those who tend to use other approaches. Random generators like those in QuickCheck (i.e. built using composition) seem much less common than e.g. only generating random ints, and operating on those directly; which makes them a less motivating example up-front. However, passing around PRNGs may be a good motivation for monadic state, and combining this with monadic parsing to get QuickCheck-style generators might be a nice climax :)

                                                                                  [2] We can do equivalent things with loops if we’re comfortable with yield; but (a) that’s a smaller subset of those that are comfortable with for, and (b) that rabbit hole leads more towards delimited continuations, algebraic effects, etc. which are alternatives to monads that are just as fascinating to think about :)

                                                                                  [3] I think the intuition for “generators” motivates the idea of composition. For “containers”, composition is merely an optimisation: we can just do multiple passes instead. Whereas for “generators”, it feels like we “don’t have the values yet”, so we need some way to plug them together before they’re “run”. IO is not a good motivator here, since imperative languages automatically compose statements for us. It seems better to come back to it after grokking parsers, random generators, etc.; even then it might be better to first describe an abstraction like a “task queue”, and only later introduce IO as a sort of “task queue for the whole language”.

                                                                                  1. 4

                                                                                    Thanks for the thoughtful reply.

                                                                                    List (and Option) are definitely good anchors for our understanding, e.g. as sanity checks when reading a generic type signature or a commutative diagram; but we also need examples that aren’t “containers”, to show why these interfaces aren’t just some weird alternative to “normal” looping. A set of examples which aren’t “containers”, but may still be familiar, are things which “generate” values; e.g. parsers, random generators, IO, etc.

                                                                                    I really love this way of looking at it, as well as you pointing out later that IO is not a good monad to introduce to people either because it is implicit in imperative code.

                                                                                    For me, there were two things that made monads really click:

                                                                                    • Build Systems a la Carte, which draws a distinction between monadic and applicative build systems – this one made me first realize that monads are a general system property much more than a Haskell curiosity
                                                                                    • The PBT example in the post, where generation isn’t hugely affected by monadic composition, but shrinking is
                                                                                2. 3

                                                                                  If you want someone who can’t spell “GHC” to understand monads, there’s only three types you need:

                                                                                  • Lists
                                                                                  • Maybe
                                                                                  • Either

                                                                                  I dunno if it’s because I learned monads when they had only recently been discovered as the solution to purely functional IO, but I expect that many programmers who have vaguely heard about monads know that they are how you do IO in Haskell. So I think a practical monad tutorial should try to bridge the gap between monads as pure algebra (lists, maybe, either) and how they are useful for impure programming.

                                                                                  (I have noticed in several cases that many programmers find it hard to get to grips with very abstract ideas, if the ideas don’t come with concrete practical examples of how the abstraction is used in practice and what benefits the abstraction provides. This is especially true the less complicated the abstraction is, which is why monads are so troublesome.)

                                                                                  The problem with list/either/maybe is that they are all parametric types, so all the monad is able to do is faff around with the layout of the values. It’s hard for a tutorial to illustrate what benefit you get from an explicit monad as opposed to less polymorphic list/either/maybe combinators.

                                                                                  So I think a monad tutorial should show an example of something more imperative such as the state monad. That allows you to show monads in use with functions that do practical things with the container type, and how the monad sequences those functions. (Perhaps also emphasise Either as the exception monad.) It’s then only a small step to monadic IO.

                                                                                  1. 3

                                                                                    ISTM that now that many languages have features like promises, there’s more relevant common knowledge among imperative programmers than there used to be. This might be an easier on-ramp than what the initial discoverers had to struggle through. A Promise<Promise<a>> can be flattened into a Promise<a>, and you can write a version of bind. Thinking of bind as “map + join” also helps avoid the “but I don’t have an a so how can I run my a -> m b function?” that I struggled with when understanding monads as they applied to things other than concrete data structures.

                                                                                  2. 2

                                                                                    Dealing with your footnote, even as someone fairly familiar with function composition, I wouldn’t immediately notice that “bind :: (a -> T b) -> T a -> T b” qualifies as function composition, but “fmap :: (a -> b) -> T a -> T b” is not. Sure, if I as down and wrote it out, it would become clear quickly, but leaving this as an exercise for the reader is just poor pedagogy.

                                                                                    1. 1

                                                                                      Would it be clearer if you considered composeM :: (a -> T b) -> (b -> T c) -> (a -> T c)? Because you can write it in terms of bind and vice-versa, provided you also have pure. (Final parens are redundant but added for clarity.)

                                                                                  3. 1

                                                                                    Relatively speaking that’s quite unpredictable.

                                                                                    Yep, thank you for clarifying. But I still think that not preserving the number of elements of a container is not the same thing as being unpredictable. For example, there are things for which you can define monadic bind (e.g. functions, https://hackage.haskell.org/package/base-4.12.0.0/docs/src/GHC.Base.html#line-828 ), for which binding means piling applications on top of each other.

                                                                                3. 2

                                                                                  Do you think it’s perverse that when I first read a rust tutorial I was perplexed about not putting semicolons at the end of a block until I decided that semicolons are just monadic bind (I don’t think I got around to writing any rust)

                                                                                  1. 3

                                                                                    It is true that semicolons are a monadic bind, but I also think that lens confuses more than it enlightens :)

                                                                                    1. 3

                                                                                      It’s the sunglasses from They Live but they show you the monads underlying all computation

                                                                                4. 70

                                                                                  People writing articles reminiscing about when junior devs just copy-and-pasted code from Stack Overflow is making me feel old.

                                                                                  1. 33

                                                                                    Same here…kids those days complaining about kids these days. :)

                                                                                    I was telling someone recently about what “browsing the literature” was like when I was working on compilers at Apple in 1990: going to the internal library down the road to read some ACM proceedings, copying references from the bibliography in the paper, asking the librarian to call Stanford to ask them to photocopy the papers Apple didn’t have, and waiting for the copies to show up in the interoffice mail. That was just 35 years ago! And getting answers to your questions on demand…if you were lucky enough to know somebody who might know the answer, you used the phone. (The lucky few in the know could try Usenet.)

                                                                                    1. 19

                                                                                      I think the op is literally 23

                                                                                      1. 12

                                                                                        Same – I remember when Stack Overflow was brand new[0], and it was introduced as some mixture of (1) a better expertsexchange and (2) a pseudo-wiki to gather and summarize knowledge that was otherwise spread across personal blogs and academic homepages.

                                                                                        At some point it transformed from a Q&A site aimed at skilled professionals into a code snippets repository for people new to the industry. Nowadays whenever I’m doing something that leads me to an SO question, usually the question is exactly what I want answered but it’s locked as a duplicate of some completely unrelated topic.

                                                                                        [0] Fun(?) story: I created the first Wikipedia article about Stack Overflow, but it got speedy-deleted by a Wikipedia administrator after about ten minutes. A lot of the early SO content was written by people who wanted to contribute computer science information to the public square, but didn’t want to deal with Wikipedia’s opaque deletionist bureaucracy.


                                                                                        Also, if you really want to feel old, write some C++ with a whole class of new hires who pronounce #include as “hashtag include”…

                                                                                        1. 1

                                                                                          who pronounce #include as “hashtag include”…

                                                                                          Out of curiosity, how do you pronounce it? :)

                                                                                          I took a couple classes in the pre-hashtag era, but I don’t recall if/how it was referred to verbally.

                                                                                          I suspect I’d never heard it called anything but a number/pound sign when I took those classes. I’m pretty sure I didn’t know octothorp(e) until years later, and I doubt I would have thought to call it a “hash” until encountering shebangs or hashtags years later.

                                                                                          1. 1

                                                                                            When I was in school the # character was universally pronounced “pound”, to the point that both students and professors would (jokingly) refer to C# as “C Pound”.

                                                                                            Oddly it wasn’t pronounced at all when naming C processor directives: #include was simply “include”. And the justification for that is “the leading pound is silent, like the leading ‘p’ in pterodactyl”. To this day I am not sure whether that professor was serious.

                                                                                      2. 10

                                                                                        Do code reviews differently. Instead of just checking if the code works, start a conversation with your team. What other approaches did they consider? Why did they pick this one? Make understanding the process as important as the end result.

                                                                                        Oof, this (the fact that this is considered “different”) hurts. This has always been how I’ve thought Code Review should run - “checking the code for correctness” is almost secondary to “ensuring that understanding of the problem domain, and the reasoning behind the chosen solution, is spread around the team”. Indeed, you can’t do the former without the latter, because Code Only Says What It Does - you can’t validate that an approach is a good fit for the problem until you understand what the constraints of the problem are.

                                                                                        1. 3

                                                                                          IME there’s two points in a project lifecycle that have lots of code reviews, neither of which are amenable to the review style you describe:

                                                                                          1. Early on there’s going to be prototyping, where the boundaries of the problem aren’t well understood and code needs to be written to check assumptions / validate implementation strategies / wring out the ambiguity introduced by natural-language design discussions. Checking for “understanding of the problem domain” (etc) only slows down progress at this point as people argue about the experimental code’s outcome in the review comments, instead of getting the code merged in so they can run the experiment.

                                                                                          2. After some sort of rough consensus has been reached in the design doc and implementation has started, allowing code reviewers to object to PRs on design grounds is basically just allowing obstinate people to re-litigate their pet theory of how the design “should have” been decided. If the design calls for the component to use SQLite and Go, then review comments like “Why aren’t you using MongoDB here? The standard language for server-side projects is Ruby, justify your decision to use Go” are (at best) noise.

                                                                                          When I do code reviews, I generally check that the code is doing what the author intends. If there’s an error in the code that causes its actual behavior to diverge from author intent, or if the author seems to have misunderstood the design doc, then that’s worth blocking on. Or if the code is unclear enough that the intent can’t be discerned without reference to the design doc, I might ask for it to be restructured (or get more inline commentary added). Either way the code review doesn’t really involve a “conversation”.

                                                                                          1. 6

                                                                                            Fascinating - I must have expressed myself poorly, because I don’t think those are counter-examples to my approach.

                                                                                            In the first case - I don’t see how “checking for ‘understanding of the problem domain’ only slows down progress at this point” is consistent with “I generally check that the code is doing what the author intends”. How can you know what the author intends (in order to check that the code does it) unless they have told you? Maybe we’re using the term “problem domain” differently - I don’t mean it in a Domain-Driven Design sense of “business unit”, but rather just the scope of the Code Review. Why is this change being made? What do we hope it’s going to achieve? If not trivially-obvious; how do we hope it will achieve that? These questions are just as relevant at the fast-and-scrappy beginning of a project as during maturity - just that the scope of the answers will be way smaller. My overall point is that it is not possible to usefully review code in isolation - it must be reviewed with a knowledge of the change-in-behaviour in the system that it is intended to bring about. Is the line const sum = 1 + 2 correct or not? Without knowing what sum is meant to hold, there is no way to tell (you can certainly recognize some forms of incorrectness in isolation - or at least invalidity or inconsistency - but recognizing correctness requires knowledge of what “correct” is)

                                                                                            In the second case - I fully agree that those kinds of comments are unproductive to landing a change.

                                                                                            • They are, however, still helpful if made in earnest, because they indicate that the commenter wasn’t aware of the design doc - in which case, that noise can be negated by a link to the design doc, and hey presto! You have improved knowledge and context among the team; precisely the aim I intended.
                                                                                            • If those comments are being made unearnestly, to sulkily complain about a particular choice, then yeah you certainly have a problem - but I suspect that kind of situation will lead to problems regardless of your code review process, that such a person will find ways to cause problems no matter what “proper process” says. I tend to find it easier and more productive to assume that my coworkers are earnestly trying to do their best to collaborate.

                                                                                            Either way the code review doesn’t really involve a “conversation”.

                                                                                            This is indeed the ideal to be striven for! A commit should stand alone, with a commit message describing the context and motivation for the change; a reviewer should be able to review the commit without needing to ask questions, because the context has already been recorded into the system. A conversation is the failure case that we fall back to if the metadata of commit message and comments has failed to provide that context.

                                                                                            1. 2

                                                                                              I don’t see how “checking for ‘understanding of the problem domain’ only slows down progress at this point” is consistent with “I generally check that the code is doing what the author intends”. How can you know what the author intends (in order to check that the code does it) unless they have told you?

                                                                                              What the author intends might not be what someone who is an expert in the area would intend, and might not be what that same author would do after a few months of experimentation have enabled the problem to be better understood.

                                                                                              In a code review the only information you have to go on is the code itself (including comments) and the commit description (or PR description, if GitHub). This is enough to provide some basic summary of what the author is trying to do in that specific change, but it doesn’t necessarily indicate understanding in the more typical sense.

                                                                                              To be more concrete, here are three of my open-source changes:

                                                                                              • https://github.com/bazelbuild/bazel/pull/16052 Bazel is a build system that downloads upstream source archives, and my change allows it to rename the files in those archives during the extraction process. I’m not anything near being an expert on source archive formats, or compression, or build systems – it’s just a quick-n-dirty patch to unblock a project I was working on (getting the Linux kernel to build on macOS).

                                                                                              • https://github.com/rust-lang/rust/pull/117156 moved some unstable APIs around in the Rust standard library. It was related to a larger project (reworking the Rust’s APIs for UNIX socket ancillary data) that ended up stalling out due to lack of upstream review bandwidth. I’m nowhere near an expert on either the Rust standard library or low-level socket options in NetBSD, and if the reviewer had tried to “start a conversation” by asking me what other approaches I’d considered, I would probably have just closed it and moved on.

                                                                                              • https://github.com/bazelbuild/rules_rust/pull/1600 is a case where the reviewer behaves as I understand you recommend – they dug deep into the change to question whether alternative approaches had been considered, spent a lot of time on “conversation”, and had me document in several ways the reasoning behind it. In the end I ended up closing that PR rather than try to continue pushing it forward.

                                                                                              In an employment setting there can be other motivations involved (i.e. want to afford rent => don’t get fired => don’t close PRs if the reviewer tries to turn them into a design review), but that’s only a motivation to endure frustration, not a justification to create it.

                                                                                              I don’t mean it in a Domain-Driven Design sense of “business unit”, but rather just the scope of the Code Review. Why is this change being made? What do we hope it’s going to achieve? If not trivially-obvious; how do we hope it will achieve that? These questions are just as relevant at the fast-and-scrappy beginning of a project as during maturity - just that the scope of the answers will be way smaller.

                                                                                              My overall point is that it is not possible to usefully review code in isolation - it must be reviewed with a knowledge of the change-in-behaviour in the system that it is intended to bring about.

                                                                                              These seem like things that would be part of the change description, either directly (like in the Linux commits) or linked to in some sort of ticket tracker (examples: https://github.com/NationalSecurityAgency/ghidra/pull/4546, https://github.com/rust-lang/rust/pull/102445).

                                                                                              I don’t think it’s necessary to go beyond that sort of plain commit message when doing experimental work early in a project’s lifecycle.

                                                                                              Documenting the reason that a particular approach was chosen, and the alternatives considered, are more likely to be part of a design document that is created after the initial experiments have identified which direction to go in. Or even after the project has launched, as part of the retrospective.

                                                                                              In the second case - I fully agree that those kinds of comments are unproductive to landing a change. […] They are, however, still helpful if made in earnest, because they indicate that the commenter wasn’t aware of the design doc - in which case, that noise can be negated by a link to the design doc, and hey presto! You have improved knowledge and context among the team; precisely the aim I intended.

                                                                                              I’m struggling with this one, because all of the places I’ve worked would have the design doc be linked from the commit description, and sometimes from the comments (linking to specific sections with functional requirements).

                                                                                              If a code reviewer commented to ask if I’d investigated alternative designs, and I responded by linking to the design doc’s ~10 page “Appendix D: Alternatives considered” section, then that would probably be … the corporate equivalent of linking to Let Me Google That For You.

                                                                                              If those comments are being made unearnestly, to sulkily complain about a particular choice, then yeah you certainly have a problem - but I suspect that kind of situation will lead to problems regardless of your code review process, that such a person will find ways to cause problems no matter what “proper process” says. I tend to find it easier and more productive to assume that my coworkers are earnestly trying to do their best to collaborate.

                                                                                              To my displeasure, and occasionally great distress, when employed I have not had the option to choose who gets hired to be a coworker. Contributing to open-source is much easier because walking away is as simple as closing the PR.

                                                                                              1. 1

                                                                                                I agree with nearly everything you’re saying, and - to me - you appear to agree with the point I’m trying to make, so I don’t know how we’re still managing to feel like we’re in opposition :)

                                                                                                What the author intends might not be what someone who is an expert in the area would intend, and might not be what that same author would do after a few months of experimentation have enabled the problem to be better understood.

                                                                                                That’s totally fine! I don’t claim that it should be. I simply claim that a reviewer must have an idea of what the author intended with a change at the time if they hope to review the code for how well it meets that intention.

                                                                                                The author’s intention might be out-of-line with business goals or higher-level technical designs, and that’s fine (not desirable, but a natural part of experimentation). Hopefully, as part of outlining their intention, that misalignment will come to light and be corrected. Even if it doesn’t, though - the reviewer still needs to know that intention in order to evaluate the change against that intention.

                                                                                                I don’t think it’s necessary to go beyond that sort of plain commit message when doing experimental work early in a project’s lifecycle.

                                                                                                I think this might be the root of the disagreement. I’m not advocating for conversations-for-conversation’s-sake, but rather saying that a reviewer needs to have this context in order to give a helpful review. Yes, the information you describe should be in a plain commit message. If it’s not - i.e. if “the only information you have to go on […] the code itself (including comments) and the commit description” is insufficient to provide a reviewer with the information they need in order to give a meaningful review - then the responsibility falls on the reviewer to request it.

                                                                                                That is - I agree with you about the amount of information that needs to be in (or “linked from”) a commit message. I am further stating that, if that information is lacking, a reviewer is not only justified, but obliged, to request it.

                                                                                                To my displeasure, and occasionally great distress, when employed I have not had the option to choose who gets hired to be a coworker

                                                                                                Fair, and relatable, but not a counter-example to my point. I, too, have coworkers who I dislike and who operate differently than I do. Nevertheless, I have still found it more productive when designing processes or setting standards for collaboration to start from a baseline assumption that collaborators earnestly want to collaborate, rather than starting from a perspective of defending myself against bad actors. The benefits of a smoother, simpler, process that works well for most people, most of the time, far outweigh the cost of setting up a “Zero Trust” process that is safe against bad actors, in my experience.


                                                                                                That said, I do think it’s notable that you’re talking about Open Source contributions rather than paid ones, where the incentives are naturally different. A contributor to Open Source is in a meaningful sense a volunteer with very little “skin in the game” for the ongoing maintenance of the system, whereas a paid employee has both a duty (via employment) and a personal obligation (making their own future lives easier) to think about maintainability. I empathize with your frustration about the requests for extra work in your third PR, and agree that that was probably unreasonable: an Open Source maintainer does still have a responsibility to ensure that that documentation exists, but - unlike a professional setting - they probably don’t have justification for requesting it from the author (who is an unpaid volunteer). They can ask for it, sure - but if they get pushback, as the actual owner or the code, the maintainer should probably step up and write it themselves, based on whatever context the author can provide.

                                                                                        2. 3

                                                                                          This reminds me of a suspicion I have that Rust’s downfall will be WASM. The reason being, we will all outsource safety to the environment instead of doing it at the language level.

                                                                                          1. 6

                                                                                            I like rust more for correctness than for safety. It’s more important to me for my program to fail less often, and lots of rust features help me do that more than many other languages, while having a large-ish ecosystem. Safety is great, but if I replaced rust with some sandboxed language that still crashes often due to mistakes I make, I wouldn’t be interested.

                                                                                            1. 4

                                                                                              Wasm doesn’t protect you from memory safety bugs, it just limits what an attacker can achieve with them. There’s still a lot that can go wrong though.

                                                                                              1. 2

                                                                                                Rust works pretty well on both sides of the WASM sandbox boundary. As a sandbox implementation it can compete directly with C and C++ in both speed and portability while providing more tools to verify correctness, and Rust when compiled to WASM produces very small binaries that don’t need special shim code or platform support (compare with, for example, Go).

                                                                                                WASM competes with sandboxing strategies such as seccomp, and at the margins can replace uses of Firecracker or gVisor (when those are being used to sandbox a single process) with infinitely better portability.

                                                                                              2. 38

                                                                                                Incidentally, I’m not a fan of languages going through the libc for interfacing with the OS. It makes cross compiling hell, while languages which just perform syscall directly just work. I think operating systems should:

                                                                                                1. Separate out a syscall wrapper library from their libc, so that non-C languages don’t have to link against a library whose main purpose is to contain a ton of utility functions meant for C programmers; and
                                                                                                2. Define a stable ABI for that syscall wrapper library, so that languages can target that ABI instead of targeting “whatever libc the build machine happens to have installed”

                                                                                                It’s frankly baffling to me that this hasn’t happened yet, especially in OpenBSD which is the main driver behind the “every interaction with the kernel must go through the libc” cult.

                                                                                                1. 26

                                                                                                  It makes cross compiling hell, while languages which just perform syscall directly just work.

                                                                                                  I think Zig’s outright trolling here:

                                                                                                  • Zig’s stdlib by default doesn’t depend on glibc and does syscalls directly, so cross-compilation just works
                                                                                                  • But you also can use Zig with glibc, and the cross-compilation still just works! When you specify a Zig target, you can request a specific version of glibc, you don’t need to compile on old Debian just to dodge symbol multiversioning.
                                                                                                  1. 27

                                                                                                    Hey I didn’t know that, that sounds excellent. Clearly, a lot of work has gone into this from Zig.

                                                                                                    However I maintain that operating systems are making this way harder than it needs to be. It’s ridiculous that every language has to bend over backwards to link against a gigantic C utility functions library with an incredibly unstable ABI and with no sort of ABI standardization across POSIX systems, just to access a few basic syscall wrapper functions.

                                                                                                    1. 11

                                                                                                      Fully agree with this, yeah!

                                                                                                      1. 3

                                                                                                        That’s just the original sin of libc: it can’t decide if it’s the OS interface or the C standard library. There is no good reason to conflate the two.

                                                                                                      2. 6

                                                                                                        When you specify a Zig target, you can request a specific version of glibc, you don’t need to compile on old Debian just to dodge symbol multiversioning.

                                                                                                        Which honestly is key. It’s always nice to say “my language does not need libc” but then in the real world you will need to interface with this anyways and you’re back to square one.

                                                                                                        1. 4

                                                                                                          you don’t need to compile on old Debian just to dodge symbol multiversioning.

                                                                                                          The fact that you are basically forced to do that with C/C++ (and also Rust, I believe) is incredibly infuriating.

                                                                                                          1. 2

                                                                                                            You aren’t forced to do that with C/C++ if you use Zig as your C/C++ cross-platform build tool. That’s an advertised feature of Zig I haven’t tried yet, so I don’t know about the limitations.

                                                                                                            1. 4

                                                                                                              The main problem is that Zig’s cross-build cleverness stops at libc, so if you have dependencies on (say) curl and openssl, you (I mean me) are better off doing a native build on the target machine.

                                                                                                                1. 5

                                                                                                                  But that’s not build cleverness, that’s a manually created build script. It’s nice that someone made that build script, but it’s dishonest to compare it to the automated smartness of Zig’s libc handling.

                                                                                                                  1. 5

                                                                                                                    It’s the same thing… In both cases I ported the build over to zig

                                                                                                                  2. 3

                                                                                                                    If I remember correctly I ran into problems trying to use Zig to cross-build some Rust bindings. This is supposed to work, but Zig was unable to find the cross-build dependencies.

                                                                                                                    I was trying out Zig as a cross-build compiler because I had read that it has a lot of cleverness so that the cross-build environment is built in. I hoped that would save me from lots of tedious setup. But I had forgotten about my C dependencies, and the error messages suggested to me that I would need to set up a cross-build environment for them. Which nullified the point of the exercise.

                                                                                                                    1. 3

                                                                                                                      Yes, I have run into this same issue as well (something to do with rodio) when trying to cross-compile rust with zig.

                                                                                                                      Granted I don’t know zig, but gave up and committed instead to a much more vanilla static build of rust with musl, and it worked fine.

                                                                                                              1. 2

                                                                                                                If you don’t mind setting it up, you could statically link to musl to avoid this. I think the rustc *-musl targets will do it automatically. (I agree it’s infuriating)

                                                                                                              2. 2

                                                                                                                Yes i found that you could even target a particular version of libc, earlier even a compiled programme on a system with newer libc woudn’t work on a system with relatively older libc, then trying to install a linux os with older libc would also fail as it wouldn’t run the newer Makefile/build-script, it just became a stupid whack-a-mole game. Now i just cross compile even from windows for any desired linux distribution a lot of not so trivial nim/c code and it just works..

                                                                                                                1. 2

                                                                                                                  Is the Zig -libc Linux only?

                                                                                                                  Because as the parent notes, there is a lot of operating systems that does not have a stable ABI for syscalls.

                                                                                                                  1. 2

                                                                                                                    libcless std is linux only AFAIK. There’s been contributions to add libcless freebsd as well.

                                                                                                                    1. 1

                                                                                                                      I guess they need to paramatise that by major versions since I think the syscalls is only guranteed stable within those.

                                                                                                                      1. 1

                                                                                                                        Old FreeBSD binaries are supported when the kernel is compiled with the COMPAT options.

                                                                                                                2. 5

                                                                                                                  I’m not a fan of languages going through the libc for interfacing with the OS. It makes cross compiling hell, while languages which just perform syscall directly just work.

                                                                                                                  That sounds backwards to me. The C library is standardized; syscalls aren’t (especially if you want to target Windows or embedded platforms!)

                                                                                                                  But I come from a world (Darwin) where the C library is just a piece of a bigger libSystem that all system calls go through, so the idea of a separate / optional libc is kinda foreign. I must be missing something.

                                                                                                                  1. 6

                                                                                                                    I think the point is “we want to free ourselves from all artifacts that C has imposed on us.” When libc is the only way to make syscalls, you can’t do that.

                                                                                                                    A library libsyscall seems to make sense, if the interface is stable (per OS), but you’re in some interesting conditional hell, that libc papers over with the “standard” interface, if you adopt that approach. Still, not sure all software implementing syscalls themselves makes sense…

                                                                                                                    1. 3

                                                                                                                      Libsyscall still imposes C constraints - it will require both a stack and calling convention.

                                                                                                                      1. 3

                                                                                                                        It means that you need to follow C conventions – for those functions in particular. A language wouldn’t have to know how to call C functions in general, it could have standard library functions implemented in assembly which calls those functions using C stack and calling conventions. Or compiler intrinsics which produces the instruction sequence necessary to call those C functions.

                                                                                                                    2. 4

                                                                                                                      We’re not talking about the C standard library as defined by the C standard, but rather as defined by POSIX and implemented by UNIX-like systems.

                                                                                                                      Both glibc in GNU and libSystem in Darwin contain loads and loads of C utility functions meant for C programs, and they bring in things like the C locale model and the C file model. And at least glibc doesn’t have a stable ABI to link against; and I suspect Apple has similar mechanisms to GNU to allow them to break ABI. In either case, almost nothing of it is relevant for a language which just wants to do its own thing, but we still need to link against all of it just to perform syscalls.

                                                                                                                      If there was a standardized (by POSIX, perhaps?) system interface C library with a standardized and stable ABI, a language could implement cross compiling from any platform to any compliant platform mostly straightforwardly, with great UX. Today, it’s hell.

                                                                                                                      1. 1

                                                                                                                        And at least glibc doesn’t have a stable ABI to link against; and I suspect Apple has similar mechanisms to GNU to allow them to break ABI.

                                                                                                                        Apple’s libSystem is just a dynamic library that exports all the system calls and C library functions. You link to it like any other dylib, and you call it using regular C calling conventions. Apple couldn’t change that without breaking literally every program in existence!

                                                                                                                        1. 2

                                                                                                                          glibc is also just a dynamic library that exports all the system calls and C library functions which you link to like any other .so and call using regular C calling conventions. Yet they change the ABI constantly in a way that’s horrible to deal with when cross compiling, they just have mechanisms in place to make that not break every program in existence.

                                                                                                                          1. 1

                                                                                                                            You could imagine Apple creating a libMach.dylib that exports only low-level API/ABI specific to macOS, leaving out libc/POSIX concepts like fopen that are implemented as wrappers on top of the native functionality, or cruft like errno that is inherently tied to the programming model of 1970s C.

                                                                                                                            If libSystem.dylib depended on libMach.dylib then existing programs would continue working. Programs that want to avoid a dependency on libc (for example because they’re written in a non-C language with its own stdlib) could link libMach.dylib directly.

                                                                                                                            1. 1

                                                                                                                              They could, but what would be the point? It wouldn’t make anything easier, or more performant. You’d just be mapping fewer code pages into your address space.

                                                                                                                        2. 1

                                                                                                                          If you are building on system X and want to target system Y, to cross compile you often need access to libc from Y on X ( this might be hard/impossible). Unless you are using a language that performs syscalls directly - in that case you don’t need the libc and cross compilation becomes simpler/possible.

                                                                                                                          1. 2

                                                                                                                            It sounds like the thing confusing me is that I’m thinking of dynamic libraries and you’re talking about static libraries. (Right?) If libc we’re dynamic your cross-compiler could just write the names of the library functions into the imports section of the binary, without having to have the actual libc.

                                                                                                                            1. 6

                                                                                                                              GNU libc is a dynamic library that has a dependency/compilation model similar to static libraries – the standard way to link against a specific GNU libc version is to have a chroot with that version installed. It’s not like macOS where you can compile on a v15.1 machine but target a minimum version of v14.0 (or whatever) via compiler flags.

                                                                                                                              The header files have #define macros that unpack to implementation-defined pragmas to override linker settings, there’s linker scripts that do further manipulation of the symbols so that a call to stat() turns into a reference to xstat64/2.0 or whatever, the .so itself might require the binary to be linked to an auxiliary .a stub library of a forward- (but not backward-)compatible version. It’s not straightforward.

                                                                                                                              Consequently, trying to cross-compile a Linux executable that links against GNU libc is a huge pain because you need to get your hands on a GNU libc binary build without going through the system package managers (that are often an assumed part of the development environment for the type of person who wants to use GNU libc).

                                                                                                                              Other Linux libc implementations (read: musl) don’t have the same limitation because they don’t have the GNU project’s … idiosyncratic … philosophy about the Linux <-> libc <-> executable relationship.

                                                                                                                        3. 5

                                                                                                                          It’s frankly baffling to me that this hasn’t happened yet, especially in OpenBSD which is the main driver behind the “every interaction with the kernel must go through the libc” cult.

                                                                                                                          Why are you surprised that this hasn’t happened in an OS where everything is C and the kernel and libc development are basically one and the same? As far as I can tell all it would do is add a layer of mapping they currently have no need for, what would the concrete benefit be from the project’s point of view?

                                                                                                                          1. 7

                                                                                                                            OpenBSD is obsessed with security, so you would think they would want to make it easier to write applications in memory-safe languages. But nope—your choices are either C, or some language that eventually pretends to be C.

                                                                                                                            (I’m being a little snarky here. I’ve gotten the impression that OpenBSD’s interest in security does not extend as far as having the humility to try to migrate away from C. That’s fine; there are plenty of solid pragmatic reasons to make that decision—but in 2025 it’s not a recipe for security.)

                                                                                                                            1. 4

                                                                                                                              That seems like a better tack, but fundamentally I’m not sure it argues for the change in question? Would a raw assembly ABI for non-C languages to bind to be in any way safer or easier than a C-ABI one? And it’s not like you’d remove the dynamic binding since openbsd is actively trying to remove raw syscalls with sycall origin verification.

                                                                                                                            2. 3

                                                                                                                              Because not everything is C. Even in the BSD world, people run lots of userspace code that’s not written in C.

                                                                                                                            3. 3

                                                                                                                              OpenBSD doesn’t promise any kind of API or ABI stability from release to release and I believe doing so is a non-goal. This got brought up the better part of a decade ago as an issue for Rust’s libc crate and they (apparently) haven’t decided what to do about it.

                                                                                                                              For better and for worse, when OpenBSD breaks something, porters usually go through the ports tree and deal with the fallout. I realize that using ports@openbsd.org as your cross-compilation tool is extremely high latency but it’s pretty reliable.

                                                                                                                              1. 2

                                                                                                                                Has OpenBSD ever made libc changes which take a function which was POSIX-compliant and makes it non-compliant? I don’t believe it has, and it’s certainly not typical. That means that there is some level of implicitly promised API stability.

                                                                                                                                1. 2

                                                                                                                                  Offhand I think they nuked gets() from orbit but that one function is so bad that it’s fair game.

                                                                                                                                  1. 2

                                                                                                                                    That was actually removed from the C standard in C11.

                                                                                                                                    1. 1

                                                                                                                                      Nice! I am glad to hear that. ❤️

                                                                                                                                      I am pretty sure OpenBSD nuked it from orbit before C11. ;)

                                                                                                                                      1. 2

                                                                                                                                        Apparently not, I am slightly surprised to discover. Tho all the BSDs had gets() print a noisy warning since the early 1990s, which was probably sufficient discouragement.

                                                                                                                            4. 2

                                                                                                                              When writing the test cases for Idol I went through three different answers to this question:

                                                                                                                              • First, since the original implementation was in Rust, I had the syntax tree types implement Debug and used plain text diff to compare them with the expected test output. This worked OK but the Debug impls sometimes needed to be customized to avoid printing irrelevant internal state of the ST values, so the time and code savings wasn’t as much as I originally hoped for.

                                                                                                                              • When rewriting the reference implementation into Go, matching the Rust Debug output proved impractical, so I switched to S-expressions. There were two big downsides to S-expressions: there’s no standard and they’re difficult to auto-format.

                                                                                                                                • There has never been a single widely-accepted document describing what S-expressions are. Every Lisp dialect has its own slightly different flavor, and every non-Lisp library for encoding/decoding S-expressions adds its own opinions into the mix. Even concepts as important as booleans (false vs #f) or string escape sequences differ between S-expression dialects. If I wanted to share test cases across languages I’d have to write my own sexpr library for each language, which is a lot of code that would itself need to be tested.
                                                                                                                                • S-expressions are difficult to auto-format consistently (is (a "b") a dict entry that should be one line, or a len=2 list that should be four lines?). The most generic method treats every (a "b") list as an actual list and indents the contents, which is hugely verbose. I could have hand-written a serializer that uses more compact output, but then I would have to write a parser and auto-formatter so that the test files’ whitespace doesn’t affect test behavior, which was yet more code.
                                                                                                                              • I switched to JSON, which has a stable specification and good-enough libraries available for ~every language. The test files can be generated by tooling (with compact indentation) or just hand-written, then in the tests they get auto-formatted prior to diff. So far this approach is working well, and I’ve used it for testing the tokenizer and parser.

                                                                                                                              Token tests (example: https://github.com/jmillikin/idol/blob/trunk/testdata/tokens/int_literals.json) contain the source code to test (a short snippet), and either the expected sequence of tokens or an expected error.

                                                                                                                              Parser tests (example: https://github.com/jmillikin/idol/tree/trunk/testdata/syntax/int_literal) are a directory containing the .idol file to parse and either a expect_ok.json file with the serialized ST, or expect_err.json with error details.

                                                                                                                              Compiler tests (example: https://github.com/jmillikin/idol/tree/trunk/testdata/schema/const) use the Idol text format, since the output of the compiler is a Schema message.

                                                                                                                              Language-specific support code lives next to the implementation(s), for Go it’s in https://github.com/jmillikin/go-idol/tree/trunk/idol/internal/testutil.

                                                                                                                              1. 5

                                                                                                                                I can’t even begin to imagine how services that don’t properly support diacritics process Chinese, Japanese, Korean, Arabic, Thai, …, names.

                                                                                                                                I’ve never flied before, but what happens if you’re being checked out on an airport and they cannot read your name? Do passports from countries speaking non Latin-based languages have romanisations of names written on them?

                                                                                                                                1. 12

                                                                                                                                  The optically-readable part at the bottom of the photo page of an IATA-compliant travel document has an ASCII-only form conforming to IATA constraints. This means that even if the name is in the Latin script, there’s an IATA ASCII version. For example Finnish ä becomes AE and ö becomes OE even though this German convention does not make sense for Finnish.

                                                                                                                                  1. 2

                                                                                                                                    Is that really a generally applied rule? I thought that was determined by the issuer, and some countries afaik even let you specify what you want it to be (within reasonable variants of writing your name obviously)?

                                                                                                                                    1. 6

                                                                                                                                      ICAO recommends (but does not require) that Ä be converted to A or AE (and Ö to O or OE) in the machine-readable part of a travel document. Maybe the rule is stricter for EU passports?

                                                                                                                                      Relevant document of ICAO requirements and recommendations for names in travel documents: https://www.icao.int/publications/Documents/9303_p3_cons_en.pdf

                                                                                                                                      1. 5

                                                                                                                                        Sorry about getting IATA and ICAO mixed up above.

                                                                                                                                        As the spec shows, the transliteration is algorithmic even if there are options for some characters. The options aren’t available to individuals: Finland does AE for ä without asking the passport applicant.

                                                                                                                                        Not sure how much of a mess it would be to change the transliteration. (IIRC, at some point Russian passports switched from French-based transliteration to English-based transliteration. There must be some failure stories about that.)

                                                                                                                                        1. 1

                                                                                                                                          If I remember right, Japan was an example of a country that does let the applicant specify. But of course there its a much bigger “gap” being bridged by the transliteration.

                                                                                                                                  2. 8

                                                                                                                                    Do passports from countries speaking non Latin-based languages have romanisations of names written on them?

                                                                                                                                    Yes, the important fields (name, country of citizenship, etc) are in Latin characters. A search for 「日本国 旅券 見本」 (Japanese passport sample) yields https://www.city.kishiwada.osaka.jp/uploaded/image/56222.png, which is detailed enough to get the gist.

                                                                                                                                    Depending on the language of origin there can be some subtleties in how names are rendered into Latin – either the government has an official romanization scheme, or they let the preferred romanization be written into the passport application (within reason).

                                                                                                                                    1. 4

                                                                                                                                      Thanks, that’s informative.

                                                                                                                                      I find it a little surprising that they don’t have both the romanisation and the local writing system.

                                                                                                                                      1. 5

                                                                                                                                        It would not surprise me if there’s a limitation to freakin’ Baudot code considering the age of some air traffic systems…

                                                                                                                                        1. 4

                                                                                                                                          My wife and son‘s thai passports have their respective names in both languages, but other details in English only.

                                                                                                                                          Their Thai ID cards have more details in both languages, but they also cram much more information in, and I believe the Thai ID system revolves much more around just knowing the ID number than relying on being able to read the details on the card quickly.

                                                                                                                                        2. 1

                                                                                                                                          It is becoming much more interesting when a country has more than one official language, and Latin transliteration is different depending on which language is used for it.

                                                                                                                                          1. 3

                                                                                                                                            Even within a single language you can still be stuck with multiple romanization schemes; for example there’s 4 or 5 in regular use with Thai, and they are all roughly equally decent if you stick with one consistently, except the main one used for road signs, which frequently renders multiple different Thai sounds with the same latin letters.

                                                                                                                                        3. 19

                                                                                                                                          I have one extended-latin character in my name. It’s ü.

                                                                                                                                          It is frequently parsed as ü when labels are printed by Australia Post.

                                                                                                                                          Alternatively, websites that want my name tell me it’s not a letter.

                                                                                                                                          1. 8

                                                                                                                                            I have an ö in my last name, and I’ve often received packages where it was replaced with o, oe or even dropped completely. A few months ago I got a package that replaced it with ÷. I’m still curious as to what happened there…

                                                                                                                                            1. 27

                                                                                                                                              A few months ago I got a package that replaced [ö] with ÷. I’m still curious as to what happened there…

                                                                                                                                              This is supposition, but ÷ is U+00F7, which was the byte assigned to œ in early drafts of Windows-28605 (aka ISO 8859-15).

                                                                                                                                              I can imagine a path from ö -> oe -> œ -> (encode to iso-8859-15-draft) -> 0xF7 -> (decode from iso-8859-15) -> ÷ existing in some ancient pre-Unicode mainframe software.

                                                                                                                                              It could also just be a bitflip, ö is 0b11110110 and ÷ is 0b11110111.

                                                                                                                                              1. 24

                                                                                                                                                We should rule out bit flip: @sny must order another package to test :)

                                                                                                                                                1. 9

                                                                                                                                                  I also wouldn’t rule out OCR error.

                                                                                                                                                2. 3

                                                                                                                                                  My last name is Küber. I was traveling back to the US through Germany once, where my passport said “Küber”, my plane ticket said “Kueber”, and my US immigration documents said “Kuber”. It took significant arguing to convince them to allow me to board my flight.

                                                                                                                                                  1. 2

                                                                                                                                                    Throwback to early 2000s! I’ve seen this often. ö is encoded as 0xf6 in latin-1 and UTF-8, while ÷ is encoded as 0xf6 in CP437, CP850, and perhaps more. Perhaps they printed the label on an old system where the code page is determined based on the printer port mode

                                                                                                                                                  2. 4

                                                                                                                                                    I do so too. I got a kick when I lived in the UK and got an UKIP mailer addressed to Mr. Küber trying to convince me that immigrants were a problem.

                                                                                                                                                  3. 18

                                                                                                                                                    I found this insightful. I hadn’t realized Carbon was specifically created out of frustration with the C++ process and definitely would not have expected them to say it so explicitly.

                                                                                                                                                    I do wonder if the two factions can even further be simplified:

                                                                                                                                                    • Faction 1: Can rebuild all of their code (libraries and executables) from scratch when needed
                                                                                                                                                    • Faction 2: Can’t

                                                                                                                                                    An ABI breaking change would at least be feasible for Faction 1. Whereas, Faction 2 obviously would be less confident to be able to make it work.

                                                                                                                                                    1. 12

                                                                                                                                                      The split presented in the article isn’t the only split, and the committee notion that there are no dialects has been fiction for a very long time.

                                                                                                                                                      On the language level, there is the split of disabling exceptions and RTTI and the other split of disabling type-based alias analysis.

                                                                                                                                                      Then there are projects that avoid the standard library and have their own strings and collections.

                                                                                                                                                      I’d be interested in seeing a survey of notable C++ code bases documenting what they do on these points.

                                                                                                                                                      1. 8

                                                                                                                                                        On the language level, there is the split of disabling exceptions and RTTI and the other split of disabling type-based alias analysis.

                                                                                                                                                        This is the one that annoys me the most. Exceptions and RTTI are simply too big for embedded systems. A stack unwinder and exception implementation takes about as much code as we have total memory. There’s simply no way that we can enable these features, yet the standard pretends they’re universal and introduces APIs that we simply can’t use safely because, without exceptions, they report error by calling abort.

                                                                                                                                                        On the standard library front, there is a defined subset: freestanding. This is a stupid subset because it’s specified as requiring a tiny subset of the standard library, but making everything else optional. Why does this matter? Because, when we ship a freestanding environment, we can ship a load of things from the hosted environment. But people can’t depend on them for portability between embedded environments.

                                                                                                                                                        1. 8

                                                                                                                                                          Whats your take on https://www.youtube.com/watch?v=BGmzMuSDt-Y ? This is a pretty deep dive inside exception handling and its relationship with embedded platforms with a few surprising takeaways:

                                                                                                                                                          • Exception handling reduces code size past a certain amount of code (specifically past a certain amount of call-to-function-that-can-fail)
                                                                                                                                                          • Existing exception handling implementations are far from optimal both from a runtime and binary size standpoint
                                                                                                                                                          1. 4

                                                                                                                                                            I don’t watch YouTube videos, but, from your summary:

                                                                                                                                                            You have to handle errors somewhere. Your choices are basically write explicit branches or emit some metadata that lets you factor it out. DWARF unwind metadata is big but it should be possible to build something smaller and that may be smaller than the branch instructions. This is probably more true in places where the error is not handled in the caller, because some metadata for unwinding can be smaller than branches to a load of function epilogue.

                                                                                                                                                            The Itanium ABI requires heap allocation to throw exceptions, which is impossible for embedded (out of memory is the most common cause of exceptions). The common alternative is SEH, but that also wouldn’t work for us because we need to handle out-of-stack and that requires destructive unwinding. It might be possible to build a combination.

                                                                                                                                                            We do implement something like old-style SEH, with a linked list of on-stack activation records, but the stack overhead if we did that for every function with cleanups would be too large. It’s only feasible because our ABI has two callee-save GPRs, and even then we need to use it sparingly.

                                                                                                                                                            For embedded systems where you have a few MiBs of RAM, the code size of the unwinder is probably amortised.

                                                                                                                                                            Doing it with C++ exceptions is complex because exceptions are built with RTTI and the type info object has a name method, which must return a C string that contains a unique name. The Itanium ABI returns a mangled name. It could also return a short unique ID, which could be much smaller, but you’re still looking at tens of bytes for each thrown or caught type, which can quickly add up.

                                                                                                                                                            1. 3

                                                                                                                                                              I don’t watch YouTube videos,

                                                                                                                                                              <3

                                                                                                                                                              I have to remember this response. I love it.

                                                                                                                                                              1. 2

                                                                                                                                                                May I recommend: https://www.youtube-transcript.io/videos/BGmzMuSDt-Y (And try the summary button.)

                                                                                                                                                                It’s on my todo list to make a CLI tool that does this.

                                                                                                                                                                (The summary doesn’t replace the transcript, but it’s a signal whether you should try reading the transcript.)

                                                                                                                                                                1. 3

                                                                                                                                                                  Interesting. Thanks for that.

                                                                                                                                                                  Machine-generated transcripts are not terribly readable and as a speedreader that does matter to me, quite a lot. I can’t zip through a verbatim transcript like I can even a 1:1 human-edited one. Better than nothing, though.

                                                                                                                                                                  I can’t remember what it was but some tech site recently published a 1-hour video with a hand-edited transcript. I timed it; I think it took me about 7 or 9 minutes to read, and I wasn’t hurrying. If hurrying I could probably halve that again.

                                                                                                                                                                  A little while ago, Charlie Stross posted something on Mastodon saying most people read about the speed that they speak. Roughly 250wpm. That really shocked me. I think my “gentle unhurried reading for pleasure” speed is ~4x that. That’s why I don’t watch videos.

                                                                                                                                                                  1. 2

                                                                                                                                                                    Same here. I don’t have a great solution. If it seems interesting, I usually put the video speed on 2X, turn on the transcript, and click through it to catch the slides.

                                                                                                                                                                    1. 2

                                                                                                                                                                      I don’t have a great solution.

                                                                                                                                                                      No, me neither.

                                                                                                                                                                      I tend to tell people I really don’t like videos and ask for other formats. As a general statement – obviously, there are exceptions – if it’s not available as text, it’s not worth the time anyway.

                                                                                                                                                                      This is why I admire David’s flat statement here.

                                                                                                                                                              2. 3

                                                                                                                                                                I rewatched it as it is a very engaging presentation. It focuses on unwind table based exception handling.

                                                                                                                                                                The main point:

                                                                                                                                                                • Exception handling code size grows with the number of functions (plus the fixed size overhead of the unwinder)
                                                                                                                                                                • Error code handling code size grows with the number of error-returning function calls

                                                                                                                                                                In typical code, the number of function calls is much greater than the number of functions, and as the code grows the overhead of the unwinder gets dwarfed by the other terms and eventually error code handling becomes more expensive in terms of code size. This is even more true in embedded where functions are even less likely to require an unwind table as dynamic allocations being banned leads to a bigger proportion of trivially destructible types.

                                                                                                                                                                Regarding the usual points against exception in embedded:

                                                                                                                                                                • Exception handling functions from the ABI are replaceable, you can write your own allocation function for exceptions that uses a custom static buffer, no need for actual free store.
                                                                                                                                                                • Most of the overhead people typically see when enabling exception is the default terminate handler implementation pulling iostream, providing your own terminate handler fixes this.
                                                                                                                                                                • libunwind adds 8kb, but the presenters own implementation is around 3kb (on ARM 32bit)
                                                                                                                                                                • -fno-rtti does the right thing, it removes RTTI information except the ones required for exception handling
                                                                                                                                                                • unwinding tables can be more compact than the equivalent assembly code because its a much simpler bytecode
                                                                                                                                                                1. 1

                                                                                                                                                                  Exception handling functions from the ABI are replaceable, you can write your own allocation function for exceptions that uses a custom static buffer, no need for actual free store

                                                                                                                                                                  That’s probably true in the general case, for us it wouldn’t fit with our threat model. There’s also the problem that C++ allows nested exceptions (so the number of in-flight exceptions is unbounded) and throwing arbitrary types (so the size of a single exception is unbounded). You can probably work around these.

                                                                                                                                                                  Most of the overhead people typically see when enabling exception is the default terminate handler implementation pulling iostream, providing your own terminate handler fixes this.

                                                                                                                                                                  That seems unlikely. At least on the big systems I most commonly use, I wrote that handler. It’s in libcxxrt and has no dependencies on the C++ standard library at all. I believe the same is true for both libsupc++ and libc++abi.

                                                                                                                                                                  The C++ personality function and associated machinery was a few tens of KiBs, last time I measured it,

                                                                                                                                                                  libunwind adds 8kb, but the presenters own implementation is around 3kb (on ARM 32bit)

                                                                                                                                                                  That seems very low. Last time I tried a stand-alone build, it was over 100 KiB for x86-64. A smaller one might be possible.

                                                                                                                                                                  unwinding tables can be more compact than the equivalent assembly code because its a much simpler bytecode

                                                                                                                                                                  I’d possibly buy this, though DWARF unwind tables are not small. Simply enabling exceptions on large codebases typically increases binary size by around 20%. I don’t think my error-handling code paths are 20% of the code size.

                                                                                                                                                              3. 2

                                                                                                                                                                The surprising thing to me was that 90% of the savings (or something like that) was just overriding the terminate handler, all because the default one pulled in iostream to print the message! With that fix, turning exceptions on added only 10k or so of code size on ARM. I may give this a try in my next embedded project.

                                                                                                                                                                1. 1

                                                                                                                                                                  I watched this with my boyfriend a few weeks ago and we were blown away by the findings. Highly recommend this to all C++ devs pessimistic about exceptions on embedded!!

                                                                                                                                                                2. 5

                                                                                                                                                                  They’re too big for non-embedded systems. Google bans them on it’s very large C++ servers. At scale you have to count bytes as closely as you do in embedded.

                                                                                                                                                                  1. 7

                                                                                                                                                                    As that link makes clear, it bans them for primarily other reasons though, and it seems unlikely that they would if size were the only downside.

                                                                                                                                                                    1. 6

                                                                                                                                                                      A friend of mine removed a small static allocation (on the order of 1k I think) from a library that’s widely used within Google and the amount of memory he saved across the fleet was mind-blowing. Another friend wanted to do climate change work and seriously considered joining the Google core libraries team because at scale any optimization you can make has significant impact on power consumption.

                                                                                                                                                                      There are obviously reasons other than resource usage to make choices, but they’re a real factor at scale.

                                                                                                                                                                      1. 16

                                                                                                                                                                        Is there a Jevon’s paradox issue with working for Google: once you free up 1% of performance by fixing some library, they will turn around and spend it on some LLM feature no one asked for?

                                                                                                                                                                        1. 1

                                                                                                                                                                          In theory no - I think they have pretty good measurements on performance regressions. In practice, given LLM enthusiasm, who knows…

                                                                                                                                                              4. 9

                                                                                                                                                                Carbon was specifically created out of frustration with the C++ process

                                                                                                                                                                Carbon and Abseil, though Abseil is older. There are choices in the C++ standard library that have turned out to be poor. I forget off the top of my head, but IIRC, especially in collection types there are some mistakes that can’t be fixed without breaking ABI that have significant performance implications. So if you can rebuild all your code and care about performance you end up rewriting parts of the standard library…

                                                                                                                                                                1. 4

                                                                                                                                                                  ABI breaking changes in C or C++ are disruptive and expensive if you are distributing binaries to customers. Even if you are using rust to build the executables you are shipping to customers, those rust executables are implicitly relying on the ABI stability of C/C++ because they are dynamically linking to C/C++ libraries in order to access system services.

                                                                                                                                                                  1. 16

                                                                                                                                                                    As a general rule ABI-breaking changes are only disruptive if you’re linking against pre-built code, such as closed-source libraries licensed from a vendor. Otherwise the ABI doesn’t matter that much, either you statically link or just ship the DLLs as part of your product.

                                                                                                                                                                    C doesn’t need to worry very much about ABIs because the language’s design encourages APIs that hew closely to the underlying platform ABI (raw pointers, fixed-size struct layouts, and such), so you don’t have to care about details like the layout of FILE. As long as the API you’re providing is stable (which might take some care, but isn’t too tricky) you can link to C code compiled 20 years ago without worries.

                                                                                                                                                                    Rust doesn’t need to worry about ABIs because most of its libraries are distributed as source code, and for the few libraries distributed as object code the language has good built-in support for exposing a language-neutral ABI-stable interface for easy invocation via FFI. Linking to system libraries isn’t a problem because either the system’s vendor knows how to produce ABI-stable shared objects (Windows, macOS) or the platform doesn’t have system libraries to begin with (Linux – the kernel ABI is stable, regardless of what the basket-case GNU libc might be doing).

                                                                                                                                                                    The reason ABIs are a problem for C++ in particular is that (1) the standard library relies so heavily on templates and value types, and (2) there’s a culture of distributing proprietary libraries as object code. Together, these properties result in dependencies that directly expose the ABI of the C++ standard library they were compiled against.

                                                                                                                                                                    1. 10

                                                                                                                                                                      As a general rule ABI-breaking changes are only disruptive if you’re linking against pre-built code, such as closed-source libraries licensed from a vendor. Otherwise the ABI doesn’t matter that much, either you statically link or just ship the DLLs as part of your product.

                                                                                                                                                                      The applicability of this varies a lot depending on the domain. Two different server apps may share little code beyond the platform’s standard libraries and so shipping everything is fine. They may even be built as container images and so can simply dynamically link a specific version of a shared library. The code sharing is small.

                                                                                                                                                                      In desktop apps, this is far less true. GUI frameworks are large. The last time I checked, the set of libraries that every KDE app linked was over 100 MiB. And that was over a decade ago. On macOS, the platform provides a load of

                                                                                                                                                                      Rust doesn’t need to worry about ABIs because most of its libraries are distributed as source code, and for the few libraries distributed as object code the language has good built-in support for exposing a language-neutral ABI-stable interface for easy invocation via FFI

                                                                                                                                                                      Or, rather, Rust doesn’t need to worry about ABIs because the percentage of Rust code in a large application is small in comparison to the amount of C/C++ code and Rust uses the stability of the C ABI for any interoperability with shared libraries. Swift went the same route initially (using the C and Objective-C ABIs for interoperability), which let them iterate quickly, but by the time Swift was mature enough for people to want to ship libraries in Swift they had to fix it (and did, quite nicely).

                                                                                                                                                                      Linking to system libraries isn’t a problem because either the system’s vendor knows how to produce ABI-stable shared objects (Windows, macOS) or the platform doesn’t have system libraries to begin with (Linux – the kernel ABI is stable, regardless of what the basket-case GNU libc might be doing).

                                                                                                                                                                      The vendor’s libraries typically use the C and/or C++ ABIs (on Windows, COM is used instead of C++). The dig at glibc is misplaced given that glibc has used symbol versioning to provide ABI stability for 20+ years. But most software packaged by Linux distros has a lot more dependencies than either. Things like ICU, GTK/Qt, and so on are all big and need to provide stable ABIs or require recompilation.

                                                                                                                                                                      The reason ABIs are a problem for C++ in particular is that (1) the standard library relies so heavily on templates and value types, and (2) there’s a culture of distributing proprietary libraries as object code. Together, these properties result in dependencies that directly expose the ABI of the C++ standard library they were compiled against.

                                                                                                                                                                      Yes and no. Templates, as traditionally implemented (compile-time reification) expose a lot of ABI surface across libraries. Swift addressed this by doing compile-time reification of generics within a library and dynamic dispatch across library boundaries. You can implement the same yourself in C++, but it’s work.

                                                                                                                                                                      The real problem for C++ (and, to a lesser extent, for C) is that the standard has a notion of a compilation unit but no notion of a library. There’s no language-level abstraction to hang ABI guarantees on. No one cares about the ABI within a compilation unit (and compilers will happily change the calling convention for static functions). No one should care about the ABI within a library, but in C++ anything that’s outside of an anonymous namespace may be referenced in other shared libraries. Modules might have fixed this if they actually worked.

                                                                                                                                                                      1. 5

                                                                                                                                                                        GUI frameworks are large. The last time I checked, the set of libraries that every KDE app linked was over 100 MiB. And that was over a decade ago.

                                                                                                                                                                        *shrug* I don’t consider 100 MB to be large, the Slack desktop application is 490 MB and it uses 10x that in RAM to render a long scrollback. The disk and memory savings from shared libraries were important 20 years ago, but nowadays there are individual command-line tools that are larger than Gtk+ and Qt put together.

                                                                                                                                                                        The dig at glibc is misplaced given that glibc has used symbol versioning to provide ABI stability for 20+ years. But most software packaged by Linux distros has a lot more dependencies than either.

                                                                                                                                                                        The symbol versioning is part of the problem with GNU libc. I can’t compile a dynamically-linked C binary on Ubuntu 24.04 and expect it to run on Ubuntu 16.04, because it’ll try to resolve libc.so symbols that don’t exist in the older version. This problem doesn’t exist when statically linking with musl.

                                                                                                                                                                        The most popular Linux distribution in the world, Android, requires packaged applications to bundle their dependencies.

                                                                                                                                                                        Things like ICU, GTK/Qt, and so on are all big and need to provide stable ABIs or require recompilation.

                                                                                                                                                                        Or just ship them with the application. Life gets a lot easier when library versions are hard-wired at build time.

                                                                                                                                                                        1. 9

                                                                                                                                                                          shrug I don’t consider 100 MB to be large, the Slack desktop application is 490 MB and it uses 10x that in RAM to render a long scrollback

                                                                                                                                                                          I believe the total on macOS for the bundle of shared libraries that are linked into every GUI process is now over 1 GiB. Slack is a bloated monster and a lot of Electron apps are moving towards things that use the platform’s native web view specifically so that the code can be shared across applications and reduce their RAM footprint.

                                                                                                                                                                          The symbol versioning is part of the problem with GNU libc. I can’t compile a dynamically-linked C binary on Ubuntu 24.04 and expect it to run on Ubuntu 16.04, because it’ll try to resolve libc.so symbols that don’t exist in the older version.

                                                                                                                                                                          No, of course you can’t. Almost nothing promises forward compatibility. You can compile on Ubuntu 16.04 and run with the glibc on 24.04 though.

                                                                                                                                                                          This problem doesn’t exist when statically linking with musl.

                                                                                                                                                                          Really? If you configure musl with a new Linux kernel and then run it on an old one, it will not give errors due to missing system calls?

                                                                                                                                                                          The most popular Linux distribution in the world, Android, requires packaged applications to bundle their dependencies.

                                                                                                                                                                          This is incredibly misleading. An Android app is four worlds:

                                                                                                                                                                          • Platform-provided native libraries.
                                                                                                                                                                          • Platform-provided ART (Java) libraries.
                                                                                                                                                                          • User-provided ART code.
                                                                                                                                                                          • User-provided native libraries (this one is optional).

                                                                                                                                                                          The overwhelming majority of code in most applications is in the first two categories. A lot of apps are <1 MiB in the APK (which includes everything in the last two categories) but pull in orders of magnitude more in the first two. Even big apps are typically under 25% per-app code.

                                                                                                                                                                          Or just ship them with the application. Life gets a lot easier when library versions are hard-wired at build time.

                                                                                                                                                                          Depending on what you include in ‘life’. When there’s a security vulnerability in libxml2, for example, do you want to have to patch it once in the system or once in every single application? How do you find all of the vulnerable versions that each app has vendored?

                                                                                                                                                                          And that doesn’t address the fact that you’re going to have 1-2 GiB of RAM consumed by multiple copies of Qt libraries if every single KDE app has its own version.

                                                                                                                                                                          1. 7

                                                                                                                                                                            No, of course you can’t. Almost nothing promises forward compatibility. You can compile on Ubuntu 16.04 and run with the glibc on 24.04 though.

                                                                                                                                                                            On other platforms there’s some notion of a target API version – macOS has -macos_version_min, Android has minSdkVersion, Windows couples the minimum runtime target to the compiler version rather than the host OS.

                                                                                                                                                                            GNU libc is unique among major platforms for not providing a way for the application’s build to specify a minimum version. There’s no preprocessor macros to control things like the fcntl -> fcntl64 transition, so binaries intended to dynamically link GNU libc.so must be built with a special sysroot.

                                                                                                                                                                            Really? If you configure musl with a new Linux kernel and then run it on an old one, it will not give errors due to missing system calls?

                                                                                                                                                                            Why would it? If the older version of the kernel is supported by libc (via fallback on -ENOSYS), there’s no reason why it should fail to run. And as far as I know, musl doesn’t have the concept of a build-time configuration for the Linux kernel version.

                                                                                                                                                                            The overwhelming majority of code in most applications is in the first two categories. A lot of apps are <1 MiB in the APK (which includes everything in the last two categories) but pull in orders of magnitude more in the first two. Even big apps are typically under 25% per-app code.

                                                                                                                                                                            I did a quick look at the applications on my phone. Firefox is 282 MB, Gboard (keyboard) is 173 MB, Kindle is 215 MB. The sizes quickly get larger if the applications contain any sort of multimedia, with games being several gigs (Genshin Impact alone is 22 GB according to the applications list).

                                                                                                                                                                            For comparison, on my desktop, libgtk-4.so.1 is 8 MB.

                                                                                                                                                                            Code size is not an important part of the installed size of modern application software.

                                                                                                                                                                            Depending on what you include in ‘life’. When there’s a security vulnerability in libxml2, for example, do you want to have to patch it once in the system or once in every single application? How do you find all of the vulnerable versions that each app has vendored?

                                                                                                                                                                            I want to patch it in every single application, so that I can control when application software behavior changes, so that I can avoid dealing with the situation where apt install rhythmbox pulls in a libxml2 update and now xsane crashes.

                                                                                                                                                                            This is the way that Android, iOS, and the macOS package managers work – if some game on my phone was built against a vulnerable libxml2 then maybe the developers of that game will fix it and push an update, maybe they won’t.

                                                                                                                                                                      2. 3

                                                                                                                                                                        I constantly link against pre-built code, such as my system’s libstdc++ or libc++. That’s open source, but they’re dynamic libraries on the user’s machine (whcih might even be my machine if I’m the user), and ABI breaks in libc++/libstdc++ would break their build until I make a new build against the new stdlib.

                                                                                                                                                                      3. 5

                                                                                                                                                                        You can ship binaries of C + C++ + Rust without depending on C++ ABI stability by treating libc++ the way the Rust standard library is treated: by including it statically in the binaries you ship and relying only on the C ABI (including COM) on the boundary between system libraries and the binary you ship.

                                                                                                                                                                        1. 7

                                                                                                                                                                          That only works to some extent. For instance I tried to link against a static libc++ on macOS for my apps but that failed at runtime because macOS’s OS frameworks also have parts implemented in c++, causing symbol collisions and ultimately crashes (for instance iirc something in ImageIO.framework would call some std:: thing that uses some global state with ABI version N, while the symbol ultimately exposed to the whole process built with a more recent clang / libc++ would have ABI n+1 - and I have to expose said symbols due to my app itself having a c++ runtime plugin API thus plugins need to be able to see the libc++ symbols)

                                                                                                                                                                          1. 6

                                                                                                                                                                            Statically linking libc++ requires some mechanism to rewrite the symbols not to collide with the system C++ standard library.

                                                                                                                                                                      4. 1

                                                                                                                                                                        I hadn’t realized Carbon was specifically created out of frustration with the C++ process

                                                                                                                                                                        Some disambiguation vis a vis the Carbon macOS API would be useful here. That was famous, in my world; this Carbon I have never heard of before in any context.

                                                                                                                                                                      5. 3

                                                                                                                                                                        I’m a bit surprised the article doesn’t mention the main issue with using threads for a high number of concurrent tasks: the memory used by each thread for its call stack.

                                                                                                                                                                        1. 10

                                                                                                                                                                          Memory usage is not the main issue with threads. Memory usage of Go’s goroutines and threads are not that different. I think it’s like 4x-8x difference? Which is not small, of course, but given that memory for threads is only fraction of memory the app uses, it’s actually not that large in absolute terms. You need comparable amount of memory for buffers for TCP sockets and such.

                                                                                                                                                                          As far as I can tell, the actual practical limiting factor for threads is that modern OSes, in default configuration, just don’t allow you to have many threads. I can get to a million threads on my Linux box if I sudo tweak it (I think? Don’t remember if I got to a million actually).

                                                                                                                                                                          But if I don’t do OS-level tweaking, I’ll get errors around 10k threads.

                                                                                                                                                                          1. 3

                                                                                                                                                                            Not touching on the rest of the comment because it’s not something I have extensive experience with, but I do want to point out that Go isn’t really the best comparison point since goroutines are green threads with growable stacks, so their memory usage is going to be lower than native threads. Any discussion about memory usage of threads probably also needs to account for overcommit of virtual vs resident memory.

                                                                                                                                                                            All of this is moot in Rust due futures being stackless, so my understanding (I reserve the right to be incorrect) is that in theory they should always use less memory than a stackfull application.

                                                                                                                                                                            1. 4

                                                                                                                                                                              That’s is precisely my point: memory efficiency is an argument for stackless coroutines over stackful coroutines, but it is not an argument for async io (in whichever form) over threads.

                                                                                                                                                                            2. 1

                                                                                                                                                                              Sorry for reading and replying to your comment so late. The minimum stack size of a goroutine is 2 kB. The default stack size of a thread on Linux is often 8 MB. Of course, the stack size of most goroutines will be higher. And similarly, it is usually possible to reduce the stack size of a thread to 1 MB or less if we can guarantee the program will never need more. Is it how you concluded that the difference was somewhere around 4x-8x?

                                                                                                                                                                              I like your point about the fact that the memory for buffers, usually allocated on the heap, should be the same regardless. Never thought of that :)

                                                                                                                                                                              1. 1

                                                                                                                                                                                The stack size of a thread can be just a few kilobytes on Linux since the pages don’t actually get mapped until accessed.

                                                                                                                                                                                1. 2

                                                                                                                                                                                  I know, but I’ve always wondered what happens when a program has hundreds of thousands of threads. Will the TLB become too large with a lot of TLB miss making the program slow? When the stack of a thread grows from 4 kB to 8kB, how is that mapped to physical memory? Does it mean there will 2 entries in the TLB, one mapping the first 4 kB, and another the second 4 kB? Or will the system allocate a contiguous segment of 8 kB, and copy the first 4 kB to the new memory segment? I have no idea how it works concretely. But I would expect these implementation “details” to impact performance when the number of threads is very large.

                                                                                                                                                                                  1. 2

                                                                                                                                                                                    I did some reading and will try to answer my own questions :)

                                                                                                                                                                                    Q: Will the TLB become too large with a lot of TLB miss making the program slow?

                                                                                                                                                                                    A: The TLB is a cache and has a fixed size. So no, the TLB can’t become “too large”. But if the working set of pages becomes too large for the TLB, then yes there will be cache misses, causing TLB thrashing, and making the program slow.

                                                                                                                                                                                    Q: When the stack of a thread grows from 4 kB to 8kB, how is that mapped to physical memory?

                                                                                                                                                                                    The virtual pages are mapped to physical pages on demand, page per page.

                                                                                                                                                                                    Q: Does it mean there will 2 entries in the TLB, one mapping the first 4 kB, and another the second 4 kB?

                                                                                                                                                                                    A: Yes. At least this is the default on Linux, as far as I understand.

                                                                                                                                                                                    Q: Or will the system allocate a contiguous segment of 8 kB, and copy the first 4 kB to the new memory segment

                                                                                                                                                                                    No.

                                                                                                                                                                                    Q: I would expect these implementation “details” to impact performance when the number of threads is very large.

                                                                                                                                                                                    A: If the stacks are small (a few kB), then memory mapping and TLB thrashing should not be a problem.

                                                                                                                                                                                2. 1

                                                                                                                                                                                  It’s 8 megs of virtual memory. Physically, only a couple of pages will be mapped. A program that spawns a million threads will use dozens of megs not 8 gigs of RAM.

                                                                                                                                                                                  1. 2

                                                                                                                                                                                    Correct, I keep forgetting about this. But assuming that each thread maps at least a 4 kB page, and that the program spawns a million threads, then it should use 1 million x 4 kB = 4 GB, and not dozens of megs? Or am I missing something?

                                                                                                                                                                                    1. 1

                                                                                                                                                                                      typo, meant to say thousand! But I guess you could flip that around and say that dozen of gigs is enough for million threads, not for a thousand!

                                                                                                                                                                                      1. 1

                                                                                                                                                                                        I like that: “a dozen of gigs are enough for a million threads” :)

                                                                                                                                                                              2. 4

                                                                                                                                                                                The stack size isn’t a problem at all. Threads use virtual memory for their stacks, meaning that if the stack size is e.g. 8 MiB, that amount isn’t committed until it’s actually needed. In other words, a thread that only peaks at 1 MiB of stack space will only need 1 MiB of physical memory.

                                                                                                                                                                                Virtual address space in turn is plentiful. I don’t fully remember what the exact limit is on 64 bits Linux, but I believe it was somewhere around 120-something TiB. Assuming the default stack size of 8 MiB of virtual memory and a limit of 100 TiB, the maximum number of threads you can have is 13 107 200.

                                                                                                                                                                                The default size is usually also way too much for what most programs need, and I suspect most will be fine with a more restricted size such as 1 MiB, at which point you can now have 104 857 600 threads.

                                                                                                                                                                                Of course, if the amount of committed stack space suddenly spikes to e.g. 2 MiB, your thread will continue to hold on to it until it’s done. This however is also true for any sort of userspace/green threading, unless you use segmented stacks (which introduce their own challenges and problems). In other words, if you need 2 MiB of stack space then it doesn’t matter how clever you are with allocating it, you’re going to need 2 MiB of stack space.

                                                                                                                                                                                The actual problems you’ll run into when using OS threads are:

                                                                                                                                                                                • An increase in context switching costs, which may hinder throughput (though this is notoriously difficult to measure)
                                                                                                                                                                                • Having to tune various sysctl settings (e.g. most Linux setups will have a default limit of around 32 000 threads per process, requiring a sysctl change to increase that). Some more details here
                                                                                                                                                                                • Different platforms behaving widely differently when having many OS threads. For example, macOS had (not sure if this is still the case) a limit of somewhere around 2000 OS threads per OS process
                                                                                                                                                                                • The time to spawn threads isn’t constant and tends to degrade when the number of OS threads increases. I’ve seen it go up all the way to 500 milliseconds in stress tests
                                                                                                                                                                                • Probably more that I can’t remember right now

                                                                                                                                                                                Of these, context switch costs are the worst because there’s nothing you as a user/developer can do about this, short of spawning fewer OS threads. There also doesn’t appear to be much interest in improving this (at least in the Linux world that I know of), so I doubt it will (unfortunately) improve any time soon.

                                                                                                                                                                                1. 4

                                                                                                                                                                                  Of these, context switch costs are the worst

                                                                                                                                                                                  What is the canonical resource that explains why context switch cost differs between the two? I used to believe that, but I no longer do after seeing

                                                                                                                                                                                  https://github.com/jimblandy/context-switch

                                                                                                                                                                                  And, specifically,

                                                                                                                                                                                  In these runs, I’m seeing 18.19s / 26.91s ≅ 0.68 or a 30% speedup from going async. However, if I pin the threaded version to a single core, the speed advantage of async disappears:

                                                                                                                                                                                  So, currently I think I don’t actually know the relative costs here, and I choose not to believe anyone who claims that they know, if they can’t explain this result.

                                                                                                                                                                                  EDIT: to clarify, it very well might be that the benchmark is busted in some obvious way! But it really concerns me that I personally don’t have a mental model which fits the data here!

                                                                                                                                                                                  1. 5

                                                                                                                                                                                    From what I understand, there are two factors at play (I could be wrong about both, so keep that in mind):

                                                                                                                                                                                    1. The time of a context switch is somewhere in the range of 1 to 2 microseconds
                                                                                                                                                                                    2. With more threads running, the number of context switches may increase

                                                                                                                                                                                    The number of context switches is something you might not be able to do much about, even with thread pinning. If you have N threads (where N is a large number) and you want to give them a fair time slice, you’re going to need a certain number of context switches to achieve that.

                                                                                                                                                                                    This means that we’re left with reducing the context switch time. When doing any sort of userspace threading, the context switch time is usually in the order of a few hundred nanoseconds at most. For example, Inko can perform a context switch in somewhere between 500 and 800 nanoseconds, and its runtime isn’t even that well optimized.

                                                                                                                                                                                    To put it differently, it’s not that context switching is slow, it’s that it isn’t fast enough for programs that want to use many threads.

                                                                                                                                                                                    1. 2

                                                                                                                                                                                      Your two comments here are some of the best things I’ve read about the topic in a while! Consider writing a blog post about this whole thing! In particular,

                                                                                                                                                                                      With more threads running, the number of context switches may increase

                                                                                                                                                                                      Is not something I’ve heard before, and it makes some sense to me (though, I guess I still need to think about more — with many threads, most threads should be idle (waiting for IO, not runnable)).

                                                                                                                                                                                      1. 2

                                                                                                                                                                                        I did write about this as part of this article about asynchronous IO, which refers to some existing work I based my comments on.

                                                                                                                                                                                    2. 3

                                                                                                                                                                                      I’d been waiting for someone who knew more about the kernel guts to comment, but I guess that’s not going to happen, so here goes.

                                                                                                                                                                                      The context switching cost shouldn’t depend on the number of threads that exist, although there were one or two Linux versions in the early 2000s with a particularly bad scheduler where it did. I don’t buy that the number of context switches (per unit time) increases with the number of threads either in most cases; in a strongly IO-bound program it will depend solely on the number of blocking calls, and when CPU-bound it will be limited by the minimum scheduling interval*.

                                                                                                                                                                                      I am not convinced about the method in the repo you linked. Blocking IO and async are almost the same thing if you force them to run sequentially. Whether this measures context switch overhead fairly is beyond my ken, but I will say that a reactor that only ever dispatches one event per loop is artificially crippled. It’s doing all its IO twice.

                                                                                                                                                                                      Contrary to what one of the GH issues says, though, it’s probably not doing context switches. Like pretty much any syscall epoll_wait isn’t a context switch unless it has to actually wait.

                                                                                                                                                                                      This isn’t a degenerate case for blocking IO and that’s enough to make up for a bit of context switching. I think that’s all there is to it.

                                                                                                                                                                                      In general, though, the absolute cost of blocking IO is lower than I think almost everyone assumes. Threads that aren’t doing anything only cost memory (to the tune of a few KiB of stack) and context switches are usually a drop in the ocean compared to whatever work your program is actually doing. I think a better reason to avoid lots of threads is the loss of control over latency distribution. Although terrible tail latency with lots more threads than cores is often observed, I don’t know that I’ve ever read a particularly convincing explanation for this.

                                                                                                                                                                                      * Although that is probably too low (i.e. frequent) by default.

                                                                                                                                                                                  2. 4

                                                                                                                                                                                    RAM is a lot cheaper nowadays than it was when c10k was a scaling challenge; 10,000 connections * 1 MiB stack is only ~10 GiB. Even if you want to run a million threads on consumer-grade server hardware (~ 1 TiB), the kinds of processes that have that kind of traffic (load balancers, HTTP static assets, etc) can usually run happily with stack sizes as small as 32 KiB.

                                                                                                                                                                                    1. 4

                                                                                                                                                                                      RAM is a lot cheaper nowadays than it was when c10k was a scaling challenge

                                                                                                                                                                                      That just means that the bar should be higher now: c10M or maybe c100M.

                                                                                                                                                                                      As for running OS threads with a small stack size, why should we have to tune that number when, with async/await, the compiler can produce a perfectly sized state machine for the task?

                                                                                                                                                                                      1. 4

                                                                                                                                                                                        That just means that the bar should be higher now: c10M or maybe c100M.

                                                                                                                                                                                        If your use case requires handling ten million connections per process, then you should use the high-performance userspace TCP/IP stack written by the hundred skilled network engineers in your engineering division.

                                                                                                                                                                                        Don’t try to write libraries to solve problems at any level of scale. Use a simple library to solve simple problems (10,000 connections on consumer hardware), and a complex library to solve complex problems (millions of connections on a 512-core in-house 8U load balancer).

                                                                                                                                                                                        As for running OS threads with a small stack size, why should we have to tune that number when, with async/await, the compiler can produce a perfectly sized state machine for the task?

                                                                                                                                                                                        Because you’ll need to tune the numbers anyway, and setting the thread stack size is a trivial tuning that lets you avoid the inherent complexity of N:M userspace thread scheduling libraries.

                                                                                                                                                                                  3. 17

                                                                                                                                                                                    Like many (most?) posts that are labelled as being against async/await in Rust, this one seems to actually be against Tokio:

                                                                                                                                                                                    In the default tokio configuration, the async runtime will schedule tasks across many threads, to maximize performance. […] Even worse, you now need to choose between std::sync::Mutex and tokio::sync::Mutex. Picking the wrong one could adversely affect performance.

                                                                                                                                                                                    and

                                                                                                                                                                                    Standard Rust threads can be “scoped”. Tokio tasks do not support it. This one is also likely never going to be fixed, since there is a fundamental issue making it impossible.

                                                                                                                                                                                    and

                                                                                                                                                                                    In async Rust, [closing a temp file via RAII] is not possible since tokio::fs::remove_file must be called from an async function,

                                                                                                                                                                                    and

                                                                                                                                                                                    Did you know Tokio will sometimes put your tasks on the heap when it believes they are too big?

                                                                                                                                                                                    and

                                                                                                                                                                                    Anytime a library adopts async/await, all its consumers must adopt it too. Aysnc/await poisons across project boundaries.

                                                                                                                                                                                    Tokio! Tokio! And more Tokio!


                                                                                                                                                                                    Reading slightly between the lines, the author seems to have started out with the assumption that they need an N:M userspace thread runtime for good I/O performance (because of a background in C# ?), then they found Tokio (an N:M userspace thread library that uses async as one part of its implementation), and they’re having trouble getting Tokio to deliver on what the author expects of it.

                                                                                                                                                                                    Maybe that’s the author’s fault, maybe it’s Tokio’s fault; I haven’t looked at the author’s code and therefore can’t judge either way. But it seems clear that it’s not Rust‘s fault, because as the author notes the async/await model works great for (1) embedded environments and (2) multiplexing I/O operations on a single thread, which are exactly the use cases that Rust’s async/await is designed to solve.

                                                                                                                                                                                    Maybe the answer is for the Rust project to intentionally de-emphasize Tokio in its async/await documentation? Over and over it seems that every time I try to use Tokio in my own projects I stumble into weird and non-Rustic behavior (e.g. the implicit spilling to the heap mentioned in this post), and every time I see an experienced programmer struggling with async/await the problems all seem to revolve around Tokio in some capacity.

                                                                                                                                                                                    1. 11

                                                                                                                                                                                      Maybe the answer is for the Rust project to intentionally de-emphasize Tokio in its async/await documentation?

                                                                                                                                                                                      I doubt that would meaningfully help.

                                                                                                                                                                                      From my perspective, it’s down to a “Nobody ever got fired for choosing IBM” attitude around Tokio, stemming from “I can trust that every dependency I might need supports Tokio. I don’t want to slam face-first into the hazard of some required dependency not supporting async-std or smol or what have you”.

                                                                                                                                                                                      I think we’re just going to have to await 😜 more things like “async fn in traits” (Rust 1.75) landing as building blocks for looser coupling between dependencies and runtimes.

                                                                                                                                                                                      (Also, Ugh. Lobste.rs apparently doesn’t have an onbeforeunload handler for un-submitted posts and I accidentally closed the tab containing the previous draft of this after getting too comfortable to compose it in a separate text editor and then paste it over.)

                                                                                                                                                                                      1. 7

                                                                                                                                                                                        Reading slightly between the lines, the author seems to have started out with the assumption that they need an N:M userspace thread runtime for good I/O performance (because of a background in C# ?), then they found Tokio

                                                                                                                                                                                        No, it’s because the existing library ecosystem is centered around tokio. If you’re not writing things yourself, you’re probably going to be using tokio. It’s the unofficial official Rust runtime.

                                                                                                                                                                                        1. 5

                                                                                                                                                                                          If you’re not writing things yourself, you’re probably going to be using tokio. It’s the unofficial official Rust runtime.

                                                                                                                                                                                          I’ve heard this a lot, but it just doesn’t seem to be true – Tokio is popular but by no means universal, and it’s silly to act as if it’s somehow more official than (for example) embassy or glommio.

                                                                                                                                                                                          Most Rust libraries are written for synchronous operation. It’s a small minority that use async at all, and even fewer of those hardcode the async runtime to Tokio. Not to mention that the use cases for which Rust has a clear advantage over Go/Java/C#/etc are things like embedded, WebAssembly, or dynamic libraries – none of which are supported by heavy N:M runtimes such as Tokio.

                                                                                                                                                                                        2. 5

                                                                                                                                                                                          To me tokio is a bunch of decisions made for me. At first when I saw it I disagreed with most of them in some way or another. After I couldn’t avoid it, I realized in the end this isn’t a half bad way of providing parallelism and for instance the alternative to “spilling to the heap” is essentially crashing.

                                                                                                                                                                                          What I think is scary about tokio for new users, and I’m planning a little post on, is that if you start going deeply async you end up unbounded, possibly with an explosion in the number of tasks depending on how your code is written. You can hit external limits, etc. Controlling that can only be done (afaict) with a tuned Arc limit from the root passing down to the overspawned task. To me it’s a small price for how easy writing and maintaining it is.

                                                                                                                                                                                          1. 3

                                                                                                                                                                                            Some of the issues are Tokio specific, some are not. Either way thinking about async/await on purely a language-level is not helpful. Everyone has to pick a runtime and due to lock-in a majority will end up with tokio. Whether the issue stems from Tokio or Rust’s language design ultimately does not matter to me as a programmer that wants my code to work.

                                                                                                                                                                                            1. 7

                                                                                                                                                                                              But you frame the article as a general critique against async/await, saying htat most of what you say should apply even to other languages!


                                                                                                                                                                                              So how about the web servers that run the web right now? Interestingly enough, nginx is written in C, and does not use async/await. Same for Apache. Those two servers happen to be the most widely used web servers and together serve two thirds of all web traffic. Both of them do use non-blocking I/O, but they do not use async/await. They seem to do pretty well regardless. (Note that my beef is specifically with async/await, not with non-blocking I/O.)

                                                                                                                                                                                              [..]

                                                                                                                                                                                              If you are a hyperscaler, you are not using async/await. For a hyperscaler, the cost of managing their server infrastructure may be in the billions. Async/await is an abstraction. You’ll not want to use it.

                                                                                                                                                                                              Are you aware that one hyperscaler, Cloudflare, replaced Nginx with an in-house proxy written in Rust using Tokio, in part due to issues with tail latencies and uneven load balancing between cores?

                                                                                                                                                                                              1. 7

                                                                                                                                                                                                AWS’s core orchestration loop uses Tokio as well.

                                                                                                                                                                                                Meta’s C++ uses Folly coroutines extensively as something quite similar to async/await.

                                                                                                                                                                                                1. 3

                                                                                                                                                                                                  Yes (link here for those interested: https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/). My reading is that the performance issues were due to nginx not reusing connections across cores, which could be solved without Tokio. Cloudflare could have opted to use mio directly for example. On the other hand I do understand their choice to use Tokio here because it probably helped them ship much faster. I would not be surprised if they eventually swap out Tokio for a custom runtime (or maybe even use mio directly) since there would probably be some extra performance to be gained.

                                                                                                                                                                                                  Note that I edited my blog post a bit to reflect the fact that hyperscalers are using async/await.

                                                                                                                                                                                                  1. 8

                                                                                                                                                                                                    I’ve looked at using mio directly. It’s much more difficult than using Tokio.

                                                                                                                                                                                                    1. 7

                                                                                                                                                                                                      My reading is that the performance issues were due to nginx not reusing connections across cores, which could be solved without Tokio.

                                                                                                                                                                                                      That’s not entirely correct. They also had difficulty with nginx’s thread-per-core model which precludes work-stealing.

                                                                                                                                                                                                      Cloudflare could have opted to use mio directly for example.

                                                                                                                                                                                                      Why would they use mio directly and implement a work stealing scheduler atop of Mio? To me, this seems like Tokio, but with extra steps.

                                                                                                                                                                                                      I would not be surprised if they eventually swap out Tokio for a custom runtime (or maybe even use mio directly) since there would probably be some extra performance to be gained.

                                                                                                                                                                                                      I would be. A work-stealing scheduler is almost certainly what most people want/need, but it is especially optimal for load balancers/proxies.

                                                                                                                                                                                                      Note that I edited my blog post a bit to reflect the fact that hyperscalers are using async/await.

                                                                                                                                                                                                      I know you edited this, but the alternative to using async/await is pervasive, feral concurrency control where people constantly spin up new thread pools. async/await, in Rust, is substantially more efficient than the alternatives that people would otherwise gravitate to.

                                                                                                                                                                                                2. 1

                                                                                                                                                                                                  Rust async/await essentially is Tokio. I have yet to see any code in the wild, or any libraries, which use async/await but not tokio.

                                                                                                                                                                                                  1. 1

                                                                                                                                                                                                    It’s extremely easy to find async code that doesn’t use Tokio. And in any case, this article claims to be a general criticism of the whole async/await paradigm, regardless of runtime or language.

                                                                                                                                                                                                3. 30

                                                                                                                                                                                                  Every time I read yet another negative post about async Rust, I feel bad for withoutboats (deliberately not doing an @-mention so as not to alert them if they don’t choose to follow this thread). withoutboats has expressed frustration with the discourse around async Rust, the way they were kicked off the project, and the slow progress since then. I wish I could donate money to some kind of pool to hire withoutboats to properly finish async Rust. I don’t have the means to individually hire someone for a reasonable wage and length of time, but maybe together we can make it happen.

                                                                                                                                                                                                  Meanwhile, I think it would be better to just quietly ignore async Rust if you feel it doesn’t benefit you.

                                                                                                                                                                                                  1. 9

                                                                                                                                                                                                    So it is very frustrating to see the discourse focused on inaccurate statements about async Rust which I believe is the best system for async IO in any language and which just needs to be finished.

                                                                                                                                                                                                    What needs to be done to finish it?


                                                                                                                                                                                                    I also feel the topic of async Rust has turned into a pile-on/circlejerk that’s entirely unproductive. That seems to be the only way that engineers can deal with complex drawn out changes.

                                                                                                                                                                                                      1. 4

                                                                                                                                                                                                        I’ll dig into it, but scanning it…

                                                                                                                                                                                                        • It starts with a change to a feature (async generators) that has itself not even landed yet?
                                                                                                                                                                                                        • It’s a ton of language minutiae which does not in any way make it clear to me what my developer experience would be in that future.
                                                                                                                                                                                                        1. 2

                                                                                                                                                                                                          Dug around to find this page which seems to indicate where the lang team’s head is at with generator syntax.

                                                                                                                                                                                                          I am always a bit sad about not having generators in Rust, given that so much of the language is about being good with holding onto things just long enough to be needed and once you have it, iterators work quite well for so much. Just really tedious to write up simple ones.

                                                                                                                                                                                                      2. 5

                                                                                                                                                                                                        That’s the first I’m hearing he was kicked off the project. I wonder who those friends were that were enough to terminate his involvement? Strange remarks all around and throwing his previous team under the bus like that.

                                                                                                                                                                                                        1. 4

                                                                                                                                                                                                          Can you, though? It seems like an increasing number of crates have async APIs, often with tokio as a dependency. If the trend continues, writing a “no-async” program may start to feel like writing a no-std program.

                                                                                                                                                                                                          1. 3

                                                                                                                                                                                                            How do I ignore it if I need to use things like gRPC?

                                                                                                                                                                                                            1. 7

                                                                                                                                                                                                              Lazy option: use the gRPC library’s synchronous API in a thread pool.

                                                                                                                                                                                                              Performant option: use gRPC’s completion queue API and (optionally) a bit of unsafe, let the gRPC library handle the async state machine.

                                                                                                                                                                                                              1. 3

                                                                                                                                                                                                                Build your own sync gRPC, with blackjack, etc.

                                                                                                                                                                                                              2. 3

                                                                                                                                                                                                                Some points:

                                                                                                                                                                                                                Async Rust is quite feat of engineering and in a way awesome. No one should be ashamed of it, or take critique of it personally, even when people point out it’s shortcommings.

                                                                                                                                                                                                                It is great that Rust supports async, but the problem is that the ecosystem decided to default to it uncritically assuming “it is strictly better”.

                                                                                                                                                                                                                It would be great if we could “just quietly ignore async Rust” but there are so many cases now where there is no choice because ecosystem provides only async-powered dependency, or where sync version is just a wrapper around async one. Sooo many cases. In my own software I don’t “ignore async Rust”, but use it along side blocking Rust, tactically, in threads where benefits of async outweigth the downsides. But >90% of the code is better off as blocking Rust. And a typical webserver doesn’t need async, so why there is no more well supported blocking-IO web servers anymore? I don’t think such a nuanced approach is getting through to the the community at large, and I wish the official channels would steer and educate the users that blocking IO Rust is OK and should be the default.

                                                                                                                                                                                                                The burnout and slow down in development of Rust is in part the effect of async’s complexity and extra work that it creates, IMO. If the ecosystem wasn’t async-default, shortcommings of async would not be so important and it would be easier to just “take the time” to solve them.

                                                                                                                                                                                                                I also agree with withoutboats that would be better if Rust just follow through with making async as “done as complete” as we can and be done with it, especially that we’re already in a “default-async Rust” situation.

                                                                                                                                                                                                                1. 4

                                                                                                                                                                                                                  I don’t understand why you can’t just ignore async Rust. Just block on the future in place and move on. This is how I worked with async libraries pre async-away. I just called .wait().

                                                                                                                                                                                                                  1. 1

                                                                                                                                                                                                                    Show me a first class non-async web framework with good community support. None left. Rouille is the closest thing from the past, but I never liked the API, it is not popular or widly used. Astra had a good idea, but developer seemed to not have enough time (Edit: I checked and seems like 2 weeks ago there was a new release, so maybe not all hope is lost). The problem is that no one wants to run blocking-IO anymore. Got to be web-scale of course, for that that 50 requests per day blog.

                                                                                                                                                                                                                    My little cli tool needs to support download something from an s3 bucket, etc.? Got to add 1M to the binary size, because can’t get aws sdk without async.

                                                                                                                                                                                                                    You’d like to stream-decompress and tar.gz archive from an s3 bucket to avoid temporary files? Good luck “just adding .wait", as now you need to deal with async vs blocking IO composition.

                                                                                                                                                                                                                    And so on. I practically never need a ultra-high-performance networking service where async IO would make a noticable difference. Faster compilation, smaller binary sizes, better language support usually would be way more valuable than async, but seems like for most people benchmark numbers is all that matters.

                                                                                                                                                                                                                    1. 2

                                                                                                                                                                                                                      Can you explain why .wait() would not work? Or the spawn_blocking equivalent?

                                                                                                                                                                                                                      1. 2

                                                                                                                                                                                                                        Show me a first class non-async web framework with good community support. None left. [..] The problem is that no one wants to run blocking-IO anymore. Got to be web-scale of course, for that that 50 requests per day blog.

                                                                                                                                                                                                                        The “problem” is that no one who wants to put a lot of effort into creating a web framework that isn’t async, presumably because the overlap between people who want a web framework, can make a web framework, and don’t want to use async, is close to zero. If the community of people who don’t want to use async want more non-async libraries, they should write them.

                                                                                                                                                                                                                        You’d like to stream-decompress and tar.gz archive from an s3 bucket to avoid temporary files? Good luck “just adding .wait”, as now you need to deal with async vs blocking IO composition.

                                                                                                                                                                                                                        No? Every async runtime that doesn’t use async file IO handles the blocking IO on a threadpool for you.

                                                                                                                                                                                                                      2. 1

                                                                                                                                                                                                                        There’s a little bit of a virality situation in Rust ecosystem because of async. For many libraries that involve IO the “best” libraries are either async-only or async-first. If you’re writing web services doubly so. I like async so I don’t mind it, but I’m sympathetic to people who don’t want to sue async but have to put up with it because they don’t have another choice.

                                                                                                                                                                                                                        1. 5

                                                                                                                                                                                                                          But you can just block on any future you run into, right? I guess if you’re using a web framework that is async you’ll run into issues there, that’s the issue?

                                                                                                                                                                                                                          1. 1

                                                                                                                                                                                                                            But that’s not just “ignoring” async, is it? There’s often some or the other complication: you either need to pull in a runtime (or at least futures) to block on a future. I haven’t done this myself because I don’t mind async, but if you have to do a multi-step async process I can imagine having to write the boilerplate to block on all of it can get tiresome.

                                                                                                                                                                                                                  2. 5

                                                                                                                                                                                                                    It seems weird to me to describe traits as not good enough because gluing together disparate libraries requires newtype wrappers, while proposing a replacement with grotesquely ambiguous semantics based on what could be generously described as academic navel-gazing.

                                                                                                                                                                                                                    This claim in particular:

                                                                                                                                                                                                                    I’m sure it won’t take much to convince you; [newtype wrappers are] unsatisfying. It’s straightforward in our contrived example. In real world code, it is not always so straightforward to wrap a type. Even if it is, are we supposed to wrap every type for every trait implementation we might need?

                                                                                                                                                                                                                    In fact it will take a lot to convince me that an extra couple lines of mechanical glue code is worse! Yes, even if you end up wrapping multiple traits!

                                                                                                                                                                                                                    The lack of orphan instances in Rust can be frustrating in times when a single library contains multiple crates, especially when I’m doing so to avoid a dependency on alloc (the lack of orphans means I can’t use Cow), but I’ve never wished for making method resolution more implicit. I’ve especially never wished to redefine it such that the programmer has to wade through hundreds of pages of type-theoretical gobbledygook to figure out why cmp() is returning the wrong value.

                                                                                                                                                                                                                    1. 6

                                                                                                                                                                                                                      Yes, that’s… why it’s a local maximum? It works really quite well, so it’s hard to find something better without first making something worse? But if you’re gonna be exploring design space that hasn’t actually been explored much, then understanding the tradeoffs involved are kinda important. Is it worth it? Probably not yet, but people keep reinventing modules and traits have some very concrete downsides. Enforcing coherence needs whole-program type information, as they demonstrate coherence can still have holes, and so on. So, let’s get off our high horse as if typeclasses don’t originate from a couple decades of academic navel-gazing and hundreds of pages of type-theoretic gobbledygook, and poke around to see if we can make something useful out of this interesting variation.

                                                                                                                                                                                                                      If you knew that it was gonna work already then it wouldn’t be science, now would it.

                                                                                                                                                                                                                      1. 1

                                                                                                                                                                                                                        Yes, that’s… why it’s a local maximum? It works really quite well, so it’s hard to find something better without first making something worse?

                                                                                                                                                                                                                        To be a local maxima is to know that a better solution exists elsewhere – it doesn’t necessarily have to have been fully described, but it must exist. The author’s claim that traits are a local maxima is equivalent to claiming that they have found a non-trait mechanism for type->method scoping that is better than traits in every way. The rest of the post fails to support that claim.

                                                                                                                                                                                                                        In particular, this part:

                                                                                                                                                                                                                        We can imagine we have a global scope of traits, and we only ever want one implementation per type in that scope. I’m going to call enforcing coherence in this way: global coherence.

                                                                                                                                                                                                                        […] Our issues with traits all orbit around requiring global coherence. Ironically, global coherence is what prevents traits from being a global maxima.

                                                                                                                                                                                                                        It’s obvious from here, if global coherence is a local maxima, local coherence is a global maxima. Now all we have to do is figure out if, and what, local coherence is.

                                                                                                                                                                                                                        is nonsense. The author assumes axiomatically that a globally-consistent mapping of types to methods is undesirable and proposes something called “local coherence” (based on … grammatical negation??), then goes off on a hunt for whatever that might be. They haven’t tried to figure out why they think globally-consistent traits are undesirable, they haven’t tried to figure out something better and then named it; they started with a name and then tried to go backwards to identify a concept.

                                                                                                                                                                                                                        The problem with their approach in this case, of course, is that in a nominative type system you do want a globally consistent association of types and traits, because otherwise the same code with the same types in different modules might have different behavior.

                                                                                                                                                                                                                        people keep reinventing modules and traits have some very concrete downsides.

                                                                                                                                                                                                                        It’s possible that traits have downsides (compared to … what?), but the author hasn’t identified any of them. The best they do is gesture vaguely in the direction of requiring boilerplate when combining libraries, which isn’t convincing in Rust (a language that is full of boilerplate).

                                                                                                                                                                                                                        If the author wants to explore actual downsides of traits, then good starting points might be:

                                                                                                                                                                                                                        • Traits form a parallel “is-a” hierarchy separate from “contains-a” value types, which makes it difficult to wrap libraries designed for OOP languages where those aren’t clearly distinguished.
                                                                                                                                                                                                                          • For example a UI framework might say that a ToggleButton extends Button and implements Widget, such that toggle_button.click() is (toggle_button as Button).click(), but this layout is difficult (or impossible) to represent in Rust because Button can’t be both a trait and a struct.
                                                                                                                                                                                                                        • The question of “sealed” traits, where implementations of a public trait can only be defined within the library that defines the trait. Sealed traits are useful because they act like a locally-extensible implicit tagged union.
                                                                                                                                                                                                                          • Haskell and Rust both require a sort of scope hack (the public trait depends on a non-exported parent), which can interfere with type inference and cause accidental un-sealing if the internal trait ends up in a public module.
                                                                                                                                                                                                                        • In Rust, adding optional methods to a trait can be a backwards-incompatible change if the method name clashes with an inherent method of a type the trait is implemented for.
                                                                                                                                                                                                                          • This doesn’t affect Haskell because it doesn’t have value-scoped function resolution, so it’s more a problem of Rust’s syntax rather than traits themselves, but it could be solved by requiring the trait methods to be brought into scope (or otherwise unambiguously referenced).

                                                                                                                                                                                                                        Note that none of these are related to the author’s wish for local scoping of trait implementations.

                                                                                                                                                                                                                        Enforcing coherence needs whole-program type information, as they demonstrate coherence can still have holes, and so on.

                                                                                                                                                                                                                        They demonstrate no such thing. They link to a GHC bug in which Haskell’s poor design leads to unexpected behavior, but that’s a problem with Haskell allowing orphan instances in -X Safe code, not with the concept of traits in and of themselves.

                                                                                                                                                                                                                        So, let’s get off our high horse as if typeclasses don’t originate from a couple decades of academic navel-gazing and hundreds of pages of type-theoretic gobbledygook, and poke around to see if we can make something useful out of this interesting variation.

                                                                                                                                                                                                                        The origin of an idea is unimportant.

                                                                                                                                                                                                                        I don’t need to read any papers on type theory to understand the behavior of Haskell’s class or Rust’s trait, and the concept has been successfully implemented in multiple languages.

                                                                                                                                                                                                                        In contrast, the author’s proposal of local implicit bindings of type-parameterized methods seems to exist only in the form of 84 pages of prose, which is within epsilon of being scrawled in crayon during an LSD trip.

                                                                                                                                                                                                                        If you knew that it was gonna work already then it wouldn’t be science, now would it.

                                                                                                                                                                                                                        I don’t see any science happening in this blog post, and I reject the idea that using word games to craft unanswerable questions is science in any sense.

                                                                                                                                                                                                                    2. 16

                                                                                                                                                                                                                      The entire argument hinges on /64 allocations not being subsettable, with VMs and containers named as examples, but in my experience everything works fine with sub-/64 address ranges.

                                                                                                                                                                                                                      VMs run with whatever address(es) they’re configured with, which might be a /64 for some reason, but is more likely to be a /96 or even a /128 (do you really need a unique address per thread…?).

                                                                                                                                                                                                                      Containers of course are assigned an IPv6 address by their runtime, and /128 is standard.

                                                                                                                                                                                                                      If you’re administrating a network and.someone shows up demanding a /60 because their laptop has vmware installed, just tell them no.