2,811 karma · joined February 5, 2009
- https://bsago.me
Some of my software projects:
- https://the.exa.website
- https://dns.lookup.dog
- https://specsheet.software
Let’s say your application tracks users, and each user needs to have at least one address. On top of that, they must flag one address as their default address, which is used when sending them formal communication, but usually they can pick which address they want to use. (Most people have one address which is their default).
The Java code to pick out a user’s default address is simple enough:
class User {
// ...
public Address getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return address;
}
}
throw new AssertionError("User has no default address");
}
}
We throw an AssertionError here instead of returning null because if a user has no default address, then that’s a bug in the program, and we want to fix it. And because it throws, we can safely assume that any address coming out of getDefaultAddress is not null.Then one day we receive a change request from a client. Some users move around a lot, and have no permanent address, and they’d like it to be changed from at least one address to at least zero addresses. Basically, make it so users don’t have to have an address anymore.
How does this affect our getDefaultAddress function? It has to handle the case where users have no addresses, because if that’s the case, then they’ll have no default address, either.
The easiest way to do it is just return null instead:
public Address getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return address;
}
}
// no default address
return null;
}
With this in place can no longer assume that the Address returned from getDefaultAddress() is an actual Address object. Which means that all the code we’ve written so far -- the code that assumes that the method doesn’t return null -- will break. So we have to find every place in the code where this method is called, and make sure that it does a null check. IDEs can help here, but they can’t help with developers who hadn’t noticed the change and continue using the method in the old way, or places where the method is called with reflection... and do you really want to test every call site to make sure you’re handling it correctly?Or we can use Optionals:
public Optional<Address> getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return Optional.of(address);
}
}
// I know I could use Java 8 streams here, just go with it :)
return Optional.empty();
}
The Optionals aren’t the most important part of this example -- it’s that the method signature changed. Our program will now no longer compile until each and every place in the code that gets a default address has been updated to specifically handle the case where there is none, because the compiler won’t accept an Optional<Address> where it expects an Address. A developer who hadn’t seen the change won’t be able to use the function in the old way by accident, as it won’t compile.This is precisely what happened to us. Changing the method to return null resulted in lots of breakage from code that we forgot to add null checks to and code written afterwards using the old signature by accident. Changing it to return an Optional meant we were forced to update everything, otherwise it wouldn’t compile. No runtime bugs. No developer slip-ups. That’s why I’m in Team Optional.
However, the second version is a lot more brittle than the first. It's relatively easy to change the code and still get it to compile, whereas you'll have a lot more trouble doing that with the first. For example:
1. If you forget to check 'order' for null, it'll still compile fine, because null is a valid value for type 'Order'. But if you try to feed an Optional<Order> to OrderEngine, it won't compile.
2. If you forget to check 'result' for null, it'll still compile fine, and then throw an NPE when you run 'result.succeeded()'. Again, if you try to call check ProcessResult::succeeded in the first example, it won't compile unless you deal with the empty-result case specifically.
3. Say that later on you want to get your stored values back out of the database... and you find a null in there. Where did it come from? If you forget to check 'result' for null and don't do the 'succeeded()' check, you can put a null in the database. Then your program will work fine until you try to call a method on it, at which point it throws an NPE far, far away from the point where you store it. By using Optional and dealing with the empty case, your code won't compile if you try to store null in the DB.
So unfortunately it will be less readable until Java devs adjust themselves to using this new type, but the benefits do exist.
This is true, but there is a distinction: if your Optional value is null, for any reason, then it's a programming error and should be fixed. If a non-Optional value is null, there's no telling whether it's "allowed" to be null or not. So I never null-check my Optionals in Java -- I'd rather they fail so I can be alerted to them.
> Multiple browser windows seems redundant with multiple tabs or the Group Tabs mentioned above.
Surely it's the complete opposite: if you have multiple windows, then why do you need Group Tabs?
(That's how I work and I still hit the wrong window a lot, so take this advice with a heaping bowl of salt)
But yeah, that's pretty much my objection -- it's not a big one, and there's room in the world for more than one type of software. I'm aware of tmux, but it just shifts the problem from vim to tmux: once you need to upgrade tmux, or your OS, or your computer, everything you've set up gets lost, and I really don't want that to happen.
I'm also aware of things like teamocil, and I even used itermocil for a while. But it turns out that the kind of environment edits I like to make are ones that I just do, rather than ones I can specify in a config file then restart to set everything up again. For example, when you say:
> If you are just exploring the code base or making small edits here and there you don't really need persistent tmux session for that.
Coincidentally I do have a relevant example for that. Last week, when I was exploring the files that make up the zoneinfo database, I put the files that I needed to keep referring to (of which there were like 5) to the side, knowing full well that they wouldn't go away. Then they just stayed there for a while as I got distracted with other stuff. This week, when I had to return to it, they're still there; and all the files from the other projects I'm working on are all in their positions, too, ready for when I switch back next week, surviving through a system restart and an editor upgrade.
However, as someone once said: "The universe tends toward maximum irony. Don't push it." If you don't close vim and keep it open for weeks, then all your environment changes are going to keep on building up, only for you to lose everything the next time you need to restart your system, or upgrade your editor or terminal, or your OS crashes, or there's a power failure, or your computer catches fire.
(And it's beside the point, but brushed metal has been dead for years by now!)
Take my first foray into vi, at its most basic level. I'm at a shell prompt, where I can use vi to edit multiple files, saving each one when I'm done with it and occasionally switching to another window to run something else, coming back when I need to edit more. But there's nothing storing my list of open files or buffers and where they are on my screen, my position in each file isn't saved; it's like vi expected me to spawn a new instance every five minutes.
When I tried emacs, which allows much more functionality inside the editor -- connecting to servers, databases, or hosting shells or debuggers -- it just made the problem worse. If I started a shell inside emacs before stopping the emacs process entirely, that shell won't be there when I get back and my files won't either. If I want to have a specific file on the right-hand side of my screen, emacs will forget where I put it. If I open a file in a new window, that file will be gone. If I want to have a file stand out by changing its background colour to a subtly-different hue, that will have changed back by the time I need to re-open my editor.
I don't like this idea; I don't want my editing environment to be some ephemeral process that's created when I start and gets destroyed when I'm finished. And while Vim has sessions and Emacs probably has three forms of saved states, they don't quite work: no saved window positions and sizes, no working with files you haven't given a name. I imagine that it's the same feeling you get when you read that something allows Emacs-like functionality: on a checklist, they might both have tick marks on each row, but when you try to use it, it doesn't work how you expect. I think that Emacs and Vim's command-line history have made its users used to this behaviour, spawning all sorts of tools to make configuring your environment easier instead of just trying to save the changes as they happen. I've seen plugins like CtrlP or projectile that enforce the idea that your environment shouldn't last, you can just set it up again each time. And that means you don't bother personalising your environment and putting things where you want them to be, because it's all going to be irretrievably gone if your computer crashes, so don't get attached. Alright, rant over.
The differences that remain are purely in the interface. For example, although they're synced the same way, VCSes tend to display issues differently than source; it's easier to filter out issue changes from the timeline, and harder to accidentally delete other people’s comments. It also makes it possible for people who aren't actual members of your project to raise and comment on the issues, which wouldn't usually be possible (unless your VCS can manage permissions for each file).
One particular use case of yours that Fossil does support is the ability to have a "wiki/" subfolder and have the web interface automatically format and display the pages inside, so you can link to them more easily if you're using the HTTP server for hosting. I think I use this feature more than the built-in wiki because it’s possible to version the docs along with the source, ensuring they never go out of sync.
It doesn't, really, but it's in the same place as the code and everything else (this is what I meant as being "on the same level"). You don't need to clone the code and then clone the tickets, for instance. When you run "fossil timeline" on your local machine, you'll see entries for tickets being closed and wiki pages being updated interspersed among the actual code commits. This is useful to me as they're as much a part of the project as the code itself. Does this answer your question?
This just keeps happening. It's not GitHub's fault that they keep getting DDoSed or have a network outage every now and again. But it's not a problem with no solution.
Fossil tracks issues (tickets), documentation, and wiki pages at the same level as it tracks code, so when you clone a Fossil repo, everything gets downloaded to your local computer. You can then respond to tickets and perform repo maintenance offline, syncing everything to the server when you're back up.
Edit: A few weeks ago I got a job a one-hour train ride away. I can commit, close tickets, and write docs on my way in, sync everything to the internet when I get to work, then do the same on my way back - even though I don't have a net connection during my commute. It works great. (Ok, not as good as not having to travel at all, but great nonetheless!)
It replaced the Send And Archive button that lives next to Send. (Adding a new one is still a bad idea, imho, but would have been borderline forgivable)
To put it another way, how am I supposed to get anything done when I'm spending all my time making sure the interface doesn't change beneath my feet?
EDIT: this was completely wrong, see below.