HN user

cytzol

2,811 karma

Personal site:

- https://bsago.me

Some of my software projects:

- https://the.exa.website

- https://dns.lookup.dog

- https://specsheet.software

Posts11
Comments304
View on HN

Embedding your assets like this isn't always an improvement. For example, I work on a site with a Go server and static content pages, and I like that I can update one of the pages and see the change instantly without having to re-compile the entire server binary just to get the new files included.

Why do you prefer having an app running (and owning the sole menu bar) when it has no GUI on screen?

Here's a story from when I was using Windows for work after using Macs for ages. I had one folder in Sublime Text open, and I wanted to close that window and then open another one. So I hit the [X] button in the corner, which closed the window, and then I instinctively went to the global menu bar at the top of the screen to go to 'File › Open' to open my new window. But of course, it wasn't there, because closing the window also got rid of my ability to access the menu bar.

And then I opened Sublime Text again, and it re-opened with the old window I wanted to get rid of.

This is why, like others in this thread, I've grown to really like the application-vs-window separation. Having a menu bar on each window, and having programs close themselves when they get down to zero windows, means I have to do my operations in a certain order (I have to open my second window before I can close the first one, I can't do it in either order) and use a UI hierarchy that I don't think makes sense (I have to use the menu bar of an existing window to open a new window, even though that operation has nothing to do with that window's contents).

Go Style 4 years ago

I found this "best practice" curious to read:

The standard net/http server violates this advice and recovers panics from request handlers. Consensus among experienced Go engineers is that this was a historical mistake. If you sample server logs from application servers in other languages, it is common to find large stacktraces that are left unhandled. Avoid this pitfall in your servers.

I don't think I've ever seen a server library — HTTP or otherwise — that didn't have a top-level "catch all exceptions" or "recover from panic" step in place, so that if there's a problem, it can return 500 (or the Internal Server Error equivalent) to the user and then carry on serving other requests.

My reasoning is that any panic-worthy programming error is almost certainly going to be in the "business logic" part of the server, rather than the protocol-parsing "deal with the network" part, and thus, recoving from a panic caused by processing a request is "safe". One incoming request could cause a panic, but the next request may touch completely unrelated parts of the program, and still be processed as normal. Furthermore, returning a 500 error but having nobody read the stacktrace is bad, yes, but it's way, way, way better than having your server crash meaning nobody can use it ever.

Oh wait, is the assumption here that your service is being run under Borg and has another 1000 instances running ready to jump in and take the crashed one's place? Is this another case of Google forgetting that people use Go outside of Google, or am I reading too much into this?

As a sidenote to your sidenote: I've noticed that the term "syntax" seems to have two different meanings nowadays. There's the technical meaning, namely "the rules that govern how characters are parsed into abstract syntax tree nodes", which in your example, would cover whether Rust shoud use `.` or `::` as a namespace separator, whether to use `[]` or `<>` for generics, that sort of thing. (Both of which have trade-offs in constraining how other parts of the language can be designed.)

But I think sometimes, people use "syntax" in a blanket "how the language looks" way — that is, whether it's symbol-heavy, whether it's word-based, whether it's information-light or information-dense, and so on. This makes it more a function of which features of expressivity the language chooses to expose, than the individual syntactic choices that determine which characters we use and for what. Again in Rust's case, it has attributes, it has namespace separators, it has the zero-tuple, it has generics, and it has lifetimes, all of which need some way to be expressed.

Don't get me wrong, you're allowed to not use a language if you don't like the way it looks visually. Or maybe it makes good use of a certain character that's hard to type on your particular keyboard layout. That's fine. I also can't decree that either of these uses of the term "syntax" are wrong. But when we're talking about language syntax, it's important to remember when you're talking about syntax, and when you're instead talking about language features. If you don't like the way lifetimes look, that's one thing; if you don't like the way lifetimes make you change the way you write code, that's another.

So I have to ask: of those four code snippets, how would you prefer to write them? What would you change? And can you get away with making those changes without breaking anything else?

Looks like the application requires macOS v12, which is only a year old. What features does it require that means it can't be backwards-compatible? Is it a SwiftUI thing?

Programming is a means to an end, and the cost of using Rust (hiring, increased development time) is often not worth it.

I agree with this. I learnt Rust before Go, and using Go makes me feel like The Oatmeal piracy guy[1]:

"I'm not sure if I should use Go to write this HTTP service. I'd lose immutability tracking, I'd lose compiler-enforced thread safety, I'd lose the powerful type system, I'd lose the comprehensive error handling, I'd suffer from a million little papercuts, I'd have to use the weird date formatting system, I'd have to check nil pointers, I'd...

...oh, it's seven days later and I've already accomplished more writing networking servers and clients in Go than I ever have in years with Rust."

This isn't to say the points raised about Go aren't true. They are true, and if a better language were available, I wouldn't stand my ground and argue their benefits, I'd switch to it. The last comment I happened to post on this website is about how Go is insufficient without its army of linting tools [2]! Yes, I'm incredibly happy to have learnt both Go and Rust as their combination has expanded my skillset and the range of programs I'm willing to write tremendously. But if someone said to me "you should just use Rust instead of Go for your production services", I'd think the "just" was doing some incredibly heavy lifting.

An article that I'd like to see is one comparing the two languages for this niche (networking servers and clients), contrasting not just the language pitfalls but the third-party libraries necessary, the iteration speed, and the choices you'll have to make up-front. My guess is that the languages would be judged more closely together.

[1] https://theoatmeal.com/comics/game_of_thrones [2]: https://news.ycombinator.com/item?id=30749921

Like many things with Go, its approach seems reasonable and simple at first, but allows you to accidentally write code that looks right but is very, very wrong. For example, what do you think this code will do?

    delaySecs := 1 * time.Second
    time.Sleep(delaySecs * time.Second)
Now I insist on using the durationcheck lint to guard against this (https://github.com/charithe/durationcheck). It found a flaw in some exponential-backoff code I had refactored but couldn’t easily fully test that looked right but was wrong, and now I don’t think Go’s approach is reasonable anymore.

Serious question: how do you square your third paragraph with your first? That is, if you're using Obsidian's features like [[square bracket link syntax]], or #tags, or inline images, aren't you effectively locked in to editors that support the same set of features?

Just a reminder to everybody that Fastmail is an Australian company, and is therefore subject to Australia's TOLA / Assistance And Access. [...] Having your e-mail provider compelled to work against your interests is no joke and you may not want to be in that situation.

This is not quite true.

The TOLA bill does allow the Australian government to compel an employee to break their product's encryption — which, yes, is dumb as hell. But Fastmail does not offer end-to-end encryption. As an Australian company, they already have had to comply with a court warrant asking them to surrender data; in other words, law enforcement does not need them to install a backdoor when they already have a front door. Your comment implies that TOLA made Fastmail less secure somehow, but this has been the case long before TOLA; the existence of that bill changes nothing.

I feel like it's important to point this out, not for the sake of pedantry, but to say that if you want truly secure encrypted e-mail, you must be in control of the encryption and decryption step, rather that having a company do that for you — you can't assume you'll be safe just because your provider isn't based in Australia. It's been a while since I've looked, but I think it would be very hard to find an e-mail provider that explicitly says it won't hand over data when presented with a valid warrant.

I use cURL a ton too, for when I want to make a one-off request, examine the response, and then throw it away. Here are some reasons why I'd reach for a tool like this:

• I want the history of every request and response to be saved by default, so if I ever need to look back to one I know it's available

• I'm sending several similar requests and I want them to share a set of variables, or, I want something in the response of one request to be used as a parameter when sending another

• I want to set a URL parameter with a bunch of symbols in without worrying about quoting

• I have so many types of requests that I'd like to organise them in a tree

• The JSON returned in a response is absolutely massive and I'd like to expand/collapse subtrees instead of viewing the whole thing as unhighlighted text

I tried the Electron version six months ago, and I wrote up its weaknesses here: https://news.ycombinator.com/item?id=28147305

EDIT: I just tried the latest beta, and I'm happy to say that scrolling the list is now much faster! On the other hand, the blurry fonts, the lack of overscroll, the non-native dropdown menus, the inability to view your vault with the Preferences window open, and the lag when resizing the window are all still there. This does not fill me with hope that the final released version is going to be any better.

If you're using Make as a command runner or a "standard entry point" to a project, instead of using it as a build system that tracks dependencies between files, I highly recommend using `just` instead: https://github.com/casey/just

It has this functionality built-in, and avoids a lot of Make's idiosyncrasies. (Not affiliated, just a fan.)

My (limited) exposure to Java was quite painful because I had to deal with layers and layer of inheritance and abstractions that at some point made it very difficult to do what I needed to do.

Hey — could you go into more detail about some of the times when inheritance and abstraction in Java were a problem, or when you had to deal with the gorilla issue?

I promise I'm not trying to catch you out or start a language war; as someone who lived in the Java world for a long time, I'm interested in which parts of it people who visited briefly have found annoying or painful to deal with.

I set up a non-public-facing IPv6-only web server last night, so the issues are fresh in my mind! I'm fortunate enough to have an IPv6-capable home connection, and the hosting provider I use (Scaleway) charges extra for assigning IPv4 addresses to machines, so I thought I'd see how easy it would be to save a bit of money and make this machine IPv6-only. I've IP-filtered the host to only my home and my other servers, so having IPv4 support should be a waste.

The machine is now running fine, but I had a few roadblocks setting it up:

• My provisioning scripts download a release of 'dry'[0] from GitHub, which does not support IPv6. I ended up assigning my new machine a temporary IPv4 address and removing it later.

• The scripts also import a key from 'keyserver.ubuntu.com'[1], which, again, does not support IPv6. Attempting to connect just timed out, and if I hadn't just solved the other issue, I would have assumed the host was down.

• There seems to be a bug in Scaleway's cloud firewall (the things it calls Security Groups), where you cannot allow inbound ICMPv6, only standard ICMP (for IPv4). This meant my pings never responded and I thought the machine wasn't up when it was up.

Basically, what I want you to take away from this post is that if you disable IPv6, it's still the case that during maintenance, things are going to break, often mysteriously and with bad error messages, but outside of maintenance, things will likely run smoothly. My machine runs Sentry, and after the problems I had setting it up, I didn't dare run the Sentry './install.sh' script with IPv4 disabled as I didn't trust it to handle that case correctly — and even if the script reported no errors, I wouldn't have trusted there to actually be no errors. Since then, though, it's been running fine, so having an IPv6-only server is certainly possible, even if you have to give in and assign it an IPv4 address at the start, then take it away again later.

[0] https://github.com/moncho/dry [1]: https://keyserver.ubuntu.com/

I'd be really happy with that! Building the functionality of errcheck[1] and ineffassign[2] into the compiler — or at the very least, into govet — would go a long way to allay my worries with Go.

I think the reason they don't do this is that it's a slight (albeit a very tiny one) against Go's philosophy of errors being values, just like any other. While the `error` type is standard and used throughout Go source code, it still just has a simple three-line definition[3] and is not treated as a special case anywhere else; there is nothing stopping you from returning your own error type if you wish. A third-party linter could simply check for the `error` type specifically, but the first-party tools should not, and there's nothing like Rust's `#[must_use]` attribute that could be used instead. I respect Go's philosophy, but I feel like pragmatism must win in this case.

[1] https://github.com/kisielk/errcheck [2]: https://github.com/gordonklaus/ineffassign [3]: https://pkg.go.dev/builtin#error

I like Go. It's useful for the things I need it for since it compiles fast into a single binary and has networking utilities in its standard library. I was used to Rust's error handling when I started, but I liked how simple Go's design was in comparison, so I stuck with it to get a proper feel for the language.

After a while, I tried using the Goland IDE, and its static analysis tool found a dozen places where I wasn't handling errors correctly: I was calling functions that return errors (such as `io.ReadCloser.Close` or `http.ResponseWriter.Write`) without assigning their results to variables, so any errors produced by them would simply be ignored. My code was compiler-error-free, go-vet warning free, and still, I was shipping buggy code.

A few months later, I try using the golangci-lint suite of linters, and again, it found even more places where I wasn't handling errors correctly: I was assigning to `err` and then, later, re-assigning to `err` without checking if there was an error in between. My code was still compiler-error-free, go-vet warning free, and now IDE-warning free — and I was still shipping buggy code.

I don't see how anyone can see this as anything other than a big ugly wart on the face of the language. It's not because it's repetitive, it's because it's fragile. Even with code I was looking at and editing regularly, it was far too easy to get wrong. I'm going to continue using Go because it still fits my purposes well, but I'm only running it on my servers, so any mistakes I make are on my head, rather than on anybody else's.

I also don't think Go's design is really amenable to things like the Option and Result types people are writing — yes, I would never have had these problems in Rust, but code written using them in Go is clunky and looks out-of-place and doesn't feel like it's the right thing to write. I wouldn't ever use the `Optional` type in the article. But it's definitely not a solution in search of a problem. There's a huge problem.

I self-host Sentry, so it's feasible, but:

• It's huge. If I run `docker-compose ps`, it lists thirty entries, one of which seems to have failed and exited.

• With that hugeness comes CPU and RAM requirements. Sentry's installer complains if you give it anything under 8 GB of memory and 4 cores.

• The UI comes with an obnoxious amount of JavaScript, and is just generally slow and clunky. I see spinning wheels a lot.

Errorpush looks like a fantastic alternative for my needs, especially if I'm able to use the Rollbar Terraform provider to configure it, as well as if there are suitable integrations for all the programming languages my services are written in.

I stopped using Genius after their mobile-first redesign. I really enjoyed reading their explanations and meanings behind songs I knew, with the lyrics in the centre of the screen and the annotations off to the side. But now, the annotations open under the lyrics, so I can't see the annotations and the lyrics at the same time, which makes it much, much harder to understand the explanations — and the rest of the page is bizarrely limited to 350 pixels wide. I can't say I'll miss Genius when it's gone anymore.

Could someone give a bird's eye view summary of why 1Password moving to Electron is bad?

There are a couple of security risks associated with moving to Electron, but I believe the company has the resources to keep up with them. So it's more of a user experience issue. I don't really mind how the app looks, as long as it looks the same as the other applications on my system, and works consistently amongst them, and it's here that Electron's bad reputation really starts to make itself known.

I downloaded the beta, signed in, and played around for a bit. Some thoughts:

• The scroll bars don't just look different, they act different. There's no overscroll, which I'm really used to. Also, when I scroll fast through a list, the scrolling indicator lags as it tries to catch up.

• Resizing the window lags a lot, and in between lag frames, you can see shades of grey as the renderer tries to catch up. 1Password 7 is damn near instant when you resize it.

• Icons and text pop into view after you've scrolled, rather than as you scroll. 1Password 7 is buttery smooth in comparison.

• Fonts look weird. 1Password 7 text looks like every other app on my system; 1Password 8 text looks blurry. Every time I look at it, it looks wrong.

• The Preferences window used to be a separate window, like it is for all other applications; now, it's a modal. You can't view the preferences and the vault at the same time.

• Oh, and it opened already using more memory than the 1Password 7 instance I've had running for _weeks_.

I really have no idea why they decided to ruin a great app like this. 1Password 8 is, as far as I can see, worse in every way than 1Password 7.

BBEdit 14 5 years ago

Thanks to using BBEdit for a decade now, I get bitterly disappointed whenever I re-open an application and it doesn't restore the windows and state it had when it closed. I've tried switching to both Emacs and Vim, but no amount of configuration nor third-party plugins could get them to work like this effectively. BBEdit works exactly how I want it to work out of the box, and I commend it for that.

The whole window system is very different from everything out there. The app does not die when you close the window.

But that's one of my favourite things about it!

I remember one time when I had to use Windows after being away from its window-based paradigm (rather than an application-based paradigm) for several years. I had a single window open in Sublime Text, and I wanted to close that window then open a new one. So I closed that window, then instinctively reached for the menu bar to select 'Open Project'. Of course, the menu bar was no longer there, because it was part of the window I just closed. And then when I opened Sublime Text to get the menu bar back, it re-opened its last open document, which was the window I was trying to get rid of.

This post illuminates two ways in which users' expectations of computers have changed since the '80s, which is when this UX was designed:

1) Users don't deal with external storage nearly as much anymore, so the "disk image" paradigm feels out of place. Before the Internet, it was more common to insert a floppy disk, drag the application from the disk to the hard drive, and then eject it again. Users were accustomed to opening and ejecting disks based on the icons on the desktop, so when disk images started to be a thing, it worked exactly like floppies did.

2) Users think of applications that have to be explicitly installed, rather than just copied, like any other kind of file.

The second one is the really surprising one: when we think of how to get applications onto our computers these days, the word is "install". You need to install a program before you can start using it. On Windows, you ran an installer program. On Linux, you ran a command. How else can you do it?

But on Classic Mac, an application was just a special kind of file, which you could drag, drop, and manipulate just like any other kind of file.

It didn't even need to be in the Applications folder! You could put it in a subdirectory, you could put it on the Desktop, you could put it wherever you liked, and it would work just the same. I think it's a darn shame that this isn't a guarantee anymore. The other day, I tried putting all of the Microsoft Office applications into /Applications/Office instead of just in /Applications, and the next time the Office updater ran, it installed into /Applications, even though that's not where the apps lived, so now I had two copies of everything. Bleh.

Why is this so much better than running a program or running a command? Well, think about how you would uninstall a program. On Windows, you'd have to run an uninstaller or find the entry in Add/Remove programs; on Linux, you'd have to run a different command. In both cases, you'd have to do something you hadn't done before — you have to know how to do something before you can do it. On Classic Mac OS, you can just delete the application by dragging it to the Trash — something you already know how to do.

Anyway, I'm not defending this approach to installing applications. But I do think it made sense at the time, and the computing landscape changed around it.

But since it’s trendy people try to apply it to everything.

When people do things you don't understand, don't just say that they're just doing it because it's fashionable. Sure, they might be — or they have their own reasons.

I use Rust in a couple of places outside of its original "systems programming" niche, where, yeah, I don't really need to track every allocation, I don't care about GC pauses, I don't need to ship one single binary, and the overhead of a runtime wouldn't bother me. Things like Web servers for side projects, or small scripts to do a task I need to automate.

However, I found that:

• The effort it took to bring Rust out of its niche, and the time it took to learn the domain-specific libraries for my use cases (Rocket for Web stuff; duct for shell script stuff) was less than I thought;

• The amount of knowledge I needed to retain to use a programming language effectively — the components of its standard library, common third-party helper libraries, how to navigate the documentation, how to fix mistakes, how to avoid traps and pitfalls, how to structure your program, how to handle differences between language versions — was much larger than I thought!

So I stick with Rust for non-systems tasks because the benefits outweigh the detriments for me.

(Granted, I was only able to do this because I already knew my way around the language and the borrow checker; if you already know, say, Python, you can make this exact same argument in reverse. But then you need to know how to wield Python for low-level programming as well as high-level programming.)

I read an article a few years ago called Java for Everything[1] (which was discussed here on HN[2]) that makes the same point, only with Java. If I had to pick an "everything language", I don't think it would be 2014-era Java, but the article did sell me on the benefits of having an "everything language" in the first place, and I feel the same benefits apply here with Rust.

[1] https://www.teamten.com/lawrence/writings/java-for-everythin... [2]: https://news.ycombinator.com/item?id=8677556

What happened to my bicycle for the mind?

The bicycle you pick depends on the problem you're trying to solve. You can divide up all programming languages into two categories:

1. Those that allow you to state your problem in terms of ideas. You write your code, the compiler or interpreter does some optimisations, and the end result runs fast enough for your use case.

2. Those that allow you to state your problem in terms of machine instructions. You demand a certain level of performance, or want every memory allocation to be explicit. The compiler or interpreter still does some optimisations, but instead of inserting more instructions implicitly if it has to, you'd rather your program straight-up fail to build.

The choices made by a language's designers will make it seem unnecessarily complicated if you're looking at a (2) language when you have a (1) problem, and will seem like it's making too many assumptions for you if you're looking at a (1) language with a (2) problem.

For example, let's go back to string concatenation, which, as you saw in Rust, looks like this:

    format!("{}{}", x, y)
And, yes, in other languages it looks like one of these two things:
    x + y
    concat(x, y)
If all you want is to end up with a "String" that's made up of those two other strings in it, this is good enough. I've done this bajillions of times over the course of my career. (Truly, I am a string-concatenating expert.)

But a String in Rust, under the hood, contains a pointer to a heap-allocated buffer. And getting one of those requires a memory allocation, and Rust makes those explicit. So the method of concatenating two strings you choose depends on what machine instructions you pick:

• If you don't really care, or if you need a new heap-allocated buffer, then `format!("{}{}", x, y)` will give you what you want.

• If `x` is already a heap-allocated String, and you know for a fact that you aren't going to need to use it again, then you can re-use the existing buffer with simply `x + y` (or something like `x.push_str(y)`).

• If `y` is a heap-allocated String but `x` isn't, you can re-use that existing buffer with `y.insert_str(0, x)` (but this requires it to copy the bytes in the buffer to make room for the string you're inserting).

• If neither is a heap-allocated String, and you don't want to allocate any more memory, you'll have to skip concatenation altogether and do something else (for example, if you're just writing the concatenated string somewhere, and don't need a buffer to put it in, then you can do `write!(somewhere, "{}{}", x, y)` which won't allocate).

Surely the computer should be doing the heavy lifting?

I find that the Rust compiler does an excellent job of doing the heavy lifting for me: I don't have to worry about iterator invalidation or use-after-free, surprise memory allocations or numeric type conversions, or unexpected performance drops when I make a small change. But this is all because I'm expecting a type (2) language, rather than a type (1) language.

It's the last part I'd like to focus on, because you may have read the above list and thought "a compiler could choose the appropriate optimisation for me!". And, well, it probably could! After all, if the only difference between the first option `format!("{}{}", x, y)` and the second option `x + y` is whether the string `x` gets used after concatenation or not, a compiler could certainly check whether this happens and optimise out the allocation if necessary.

But then you need to worry about keeping this optimisation when the code changes. Sticking with the example, suppose you have code like this in some hypothetical (1) language that allows you to express ideas but optimises them the best it can, where `x` and `y` are Strings still:

    var frobozz = x + y;
And then someone on your team makes this change:
    var frobozz = x + y;
    /* skip a couple dozen lines of code */
    var gunther = x + z;
A code change that starts using `x` for a second time now results in an allocation being introduced a couple dozen lines above from where the code change was, because it can now no longer be optimised out. If this is still good enough for you, because you're solving a (1) problem, then there's nothing wrong with this. But if you're solving a (2) problem, then the language has kind of... let you down.

So don't be sad! It just depends on what problem you want the language to solve for you.

People install things and click rapidly thru the startup screens.

While this is true, and there are definitely cases of people not reading the text and accepting whatever, Apple has a long way to go here. On the payment screen, the text showing the price and the recurrence is way too small, and they're both located under an eminently-skippable "Policy" paragraph. It's no surprise that users are skipping this user-unfriendly screen.

This Twitter thread has some examples of how it can be improved: https://twitter.com/rjonesy/status/1358161301973979139