Hacker Timesnew | past | comments | ask | show | jobs | submit | mswphd's commentslogin

hard to give recommendations without more information. For example

1. what languages does he know? there are boardgames that are localized into other languages. Probably the easiest route tbh.

2. what kinds of games does he like? for example, many boardgames have very little english on the game pieces. think any game that uses a standard poker deck, e.g. solitaire, or many others. Mahjong is another example though, as is dominos).

There are some modern boardgames that might also be fine, namely ones that discourage communication in the first place. It's common in co-op boardgames. For example, the Lord of the Rings trick taking game is 1-4 players, and during gameplay there is no discussion allowed. Game pieces can be separated into two categories

1. scenario-specific ones, which have text on them/must be read to be understood/played. You could maybe translate them? or it may have been localized for a language he's literate in. I don't know.

2. secnario-independent ones, which are (functionally) poker cards.

For this game you only need to share language when understanding the scenario-specific cards, and when planning strategy before each scenario starts. I would be comfortable playing the game with someone I don't share a language with if

1. we both know the game (this would be the hard part), and

2. we have two copies of the game, so we each can read our scenario-specific cards in our own language, and

3. we struggled through with a translation app before each scenario starts, if we want to discuss strategy.


He only speaks Spanish. He plays with my Mom who also only speaks Spanish. They play Solitaire, dominoes and Sequence.

for context: in rust the "translation unit" is a crate (in C++, each source file is its own translation unit). So you only get parallelism across crates when compiling in rust. When you have a single-crate project, this means that

1. you get parallelism across all your dependencies, but

2. your (final) crate is serial.

Splitting your crate can therefore get you parallelism back in step 2, which can be a (compilation) perf win. You can also get a caching benefit when there aren't changes, but even absent that you can get wins.

It can come at the cost of runtime performance though, as rust won't (by default) do things like inline across translation units iirc. You can get back some of this perf back via various tricks (for example link-time optimization).

Looks like the rust performance book has some writing on these tradeoffs. I haven't read these articles, but the table of contents on the left seems reasonable enough.

https://nnethercote.github.io/perf-book/build-configuration....

Again I haven't read that (so I'm assuming it says the following), but you can get "best of both worlds" by configuring debug builds to not do LTO (= faster compilation speed, slower runtime), and release builds to do LTO (= faster runtime, slower compilation speed). There are also variants of LTO that make various tradeoffs you could look into.

That article also links to the `cargo-wizard` subcommand of cargo

https://github.com/Kobzol/cargo-wizard

it won't auto-split crates for you (of course), but it does seem to give you some default configurations of `cargo`, one of which configures for faster compilation speed. could be an easy way to mess around with things.


if you have an OOM and want to log why, if your logging allocates it will likely fail as well. You could in principle work around this with enough effort, but "properly" handling OOM is typically much more trouble than its worth.

There's a number of things about rust that help compared to other statically typed languages.

1. the compiler gives very high quality error messages. It helps humans, and also helps LLMs

2. Rust reduces memory management to local reasoning (via the borrow checker). This means that it performs well even as context grows, because checks in one function/module are well-encapsulated to that function/module.

3. Rust can more easily obtain this encapsulation for more general properties than many other statically typed languages. In particular, rust's type system is very strong, so it's easy to take a function `func(x: T)` that relies on some implicit assumption on `x` (say that it is non-zero), and turn it into an explicit requirement. By this, I mean you define `pub struct NonZero(T)`, and provide constructors `pub try_new(t: T) -> Result<NonZero<T>, _>` that error if the condition doesn't hold. If you additionally only provide public methods on `NonZero<T>` that uphold the invariant, you can lift runtime runtime assertions to the type level. This is both good practice, and helps out LLMs quite a bit.

This is to say that rust makes it quite easy to encapsulate implementation details (both regarding memory management, as well as other details) essentially completely. Sometimes you still have invariants that need care/can't be encapsulated in the type system, but such invariants should be marked `unsafe`, so it can be easier to audit the LLM's output.

Anyway, the "more constraints to balance" is only problematic if all the constraints are inter-dependent. It's definitely possible to get LLMs to generate spaghetti code like this, but the way you fix it is the way you fix similar issues in other languages.


> This is both good practice, and helps out LLMs quite a bit.

Don’t get me wrong, I like this aspect of Rust, but I can’t make heads or tails as to whether it helps or if they just have to iterate more to figure out how to make something work. LLMs already do pretty well with a comment “this value can’t be zero” in my experience, so I’m unsure how much value the static typing provides. Maybe it lets you get by with a lower quality model, but that model will likely just spend more tokens on iteration so I can’t discern an obvious win. (shrug) I hope I’m wrong though—if I can have super fast code with the ease of LLM generation then I’m happy.

> Rust reduces memory management to local reasoning (via the borrow checker). This means that it performs well even as context grows, because checks in one function/module are well-encapsulated to that function/module.

I don’t think this is true, right? Changing a single lifetime in a function signature can easily propagate across your entire program. Maybe I’m just a Rust noob, but any time I change a field from owned to borrowed or vice versa I have to propagate that change pretty broadly, which to my mind implies consuming a lot of the context window. Garbage collection (I know, ewww, shame on me, etc) allows for local reasoning in a much more meaningful way however morally impure it may be. :)


a few things

1. thiserror just does codegen of the "standard" enum things people do. if you find debugging thiserror difficult, just write out the enums manually. sure it's uglier, but (roughly) equivalent. so its preferable as synctatic sugar for enums, but doesn't have any technical benefits (in the same way that syntatic sugar never really does).

2. for boxed errors, you only get allocations on your error path. Hopefully this is a cold path so it shouldn't matter.

There is a general theme behind rust error handling though which it can be good to internalize. In particular, the more details of your errors you encode in the type system, the more powerful things are. Any error type could just be

pub struct MyError(String)

the issue is that this gives very little information to a caller on what to do with your error. If you have no callers (e.g. are making a binary) it's fine, and (roughly) what `anyhow` does.

That all being said, when designing errors a natural question to ask is "can my caller do anything meaningful with this error"? For example, in Rust stdlib, `Vec::push` can allocate. This allocation can fail, which panics. "Proper" error handling would use the fallible allocation API, and propagating an OOM error or whatever through results. For most applications, this is not an error that is worth investing that much time into guarding against, so using the (potentially panicing) `Vector::push` makes things easier.

You can take this same perspective in other settings as well, in particular separating out errors into

1. structured data, that a caller should be able to extract/process to handle the error, and 2. unstructured data, that is more used for logging, and you expect the caller to pass up the call stack without inspecting themself.

Handling both types of these errors with `thiserror` can be tedious for little benefit. I've found it useful to instead solely use `thiserror` for category 1, and category 2 does other things. This could be using `.expect(...)`. There are some crates that make this nicer (e.g. `error_stack`). But the point is that it can significantly clean things up if you only encode in your error enums failures that you expect someone to handle, rather than just e.g. log.

This does somewhat validate your point that the "right way" I've been experimenting with (and mentioned above) is not just "use this-error".

Also: a big issue with `thiserror` is the tedium of handling the large error enums (or giving up on using it "properly" and shoving together multiple error variants in some unstructured error type). that is somewhat better in the LLM era, as you can have the LLM handle the tedium.


also thiserror isn't incompatible with io::Error? all thiserror does is do code generation for "typical" enum errors. The errors it generates are normal rust code though, and the fact that they're generated by thiserror shouldn't really matter?

I don't get where the idea comes from that the popular error crates make error handling complicated in Rust. Because you're right, all thiserror is doing is giving you a shorter syntax for writing error enums. You could write the exact same things out by hand if you wanted to, and from the library user's side nothing would change.

As for anyhow, if a library ever exposes that, then that's just the author being lazy and not doing errors correctly. It's the equivalent of doing throw Exception("error!") in C#.


circular breathing is useful for other instruments as well, though it's not typically a technique that's necessary until you get to fairly high levels.

there is the other (significant) issue, that wealth (and its many benefits) are inherited, and by all indications the exponential advantage seems to pass down through generations (at least recently).

we used to ship mass lists of addresses and phone numbers to people in each town and it was fine/appreciated.

You could also easily opt out with the single entity that shipped that information.

Yes, but getting an unlisted number was considered weird and against the norm even if possible. Even in the early 2000s when I dropped my landline, my parents were aghast - "if you do that, you won't be in the phone book! How will anyone get in contact with you?"

And you often had to pay for the privilege... A dollar a month for them to not put your name and number in the phonebook.

Yeah and back then it wasn't used as a sort of UUID to track every single thing you do in your life... Different times

You ever had a bump in the night my guy?

Or a stalker?


isn't the whole point of C that it's portable assembly though? needing to understand the target platform/compiler's behavior to write correct code seems to cut against that claim quite a bit.

No. What gives that idea? The language doesn't even fix the data size of its primary numerical type. No way anyone thought that was portable.

Is this sarcasm? I thought C didn't fix the size of int because they were trying to make C programs "portable" between architectures with different natural word sizes. It was a mistake, but I remember that as being the stated reason. I'm happy to be corrected if I'm misremembering my history though.

Why would it be a mistake? It's efficient for the target platform.

The same code can be compiled for different platforms, yes, but the assembly and machine code will vary significantly, so it could behave differently. Porting to a new platform was usually a very complex process, but the code produced was efficient. Nobody seems to care about this nowadays, though, it seems.


Except the way it's done in C is illogical to the point where it negates the value proposition.

People expect numbers to support specific ranges and it is fine to define the data types numerically rather than as a concrete bit pattern, but C just takes the cake.

Char is at least 8 bits, short is at least 16 bits, int is at least as big as short (genius idea), long is at least 32 bits, long long is at least 64 bits.

The point of "int" is to be the integer equivalent of size_t and therefore be of word size.

But nobody uses int like that. Everyone assumes it's a 32 bit datatype when it isn't.

The use case where you port existing C code to a microcontroller is extremely unappealing, because the number range gets changed under your feet. When I've had to work on embedded software everyone just used int8_t, int16_t, int32_t, int64_t for portability instead.


I suppose one could say that they didn't fix the data size so the language would be portable. But I can't see how the intent was that programs would be portable, if you define portable to mean 1:1 functioning across differing platforms.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: