Most active commenters
  • johnfn(5)

←back to thread

517 points bkolobara | 18 comments | | HN request time: 0.429s | source | bottom
1. johnfn ◴[] No.45049034[source]
I think Rust is awesome and I agree with that part of the article.

What I disagree with is that it's the fault of Typescript that the href assignment bug is not caught. I don't think that has anything to do with Typescript. The bug is that it's counter-intuitive that setting href defers the location switch until later. You could imagine the same bug in Rust if Rust had a `set_href` function that also deferred the work:

    set_href('/foo');

    if (some_condition) {
        set_href('/bar');
    }
Of course, Rust would never do this, because this is poor library design: it doesn't make sense to take action in a setter, and it doesn't make sense that assigning to href doesn't immediately navigate you to the next page. Of course, Rust would never have such a dumb library design. Perhaps I'm splitting hairs, but that's not Rust vs TypeScript - it's Rust's standard library vs the Web Platform API. To which I would totally agree that Rust would never do something so silly.
replies(7): >>45049104 #>>45049184 #>>45049492 #>>45049625 #>>45049709 #>>45052032 #>>45052981 #
2. flohofwoe ◴[] No.45049104[source]
Objection your honor ;)

A 'setter' should never ever cause an action to be triggered, and especially not immediately inside the setter.

At the least change the naming, like `navigate_to(href)`.

But in the browser environment it's also perfectly clear why it is not happening immediately, your entire JS code is essentially just a callback which serves the browser event loop and tells it what to do next. A function which never returns to the caller doesn't fit into the overall picture.

replies(1): >>45049332 #
3. masklinn ◴[] No.45049184[source]
Technically Rust could hint at the semantics, based on whether `set_href` returned `()` or `!`. However the “erroneous” usage would not be surfaced in the case of a conditional redirect indeed (for a non-conditional one you may notice that the following code is not dead).
4. johnfn ◴[] No.45049332[source]
That’s a good point. I actually modified my comment because I assumed everyone would take for granted that no work should be done in a setter :)

> A function which never returns to the caller doesn't fit into the overall picture.

Hmm, not sure about this. On the node side, you can process.exit() out of a callback. If setting href worked like that, I think it would be less confusing.

replies(1): >>45050371 #
5. bkolobara ◴[] No.45049492[source]
Thanks! I should have clarified a bit better that example.

The point I was trying to make is that Rust's ownership model would allow you to design an api where calling `window.set_href('/foo')` would take ownership of `window`. So you would not be able to call it twice. This possibility doesn't exist at all in TypeScript, because it doesn't track lifetimes.

Of course, TypeScript can't do anything here either way. Even if it had knowledge of lifetimes, the JavaScript API already existed before and it would not be possible to introduce an ownership model on top of it, because there are just too many global variables and APIs.

I wanted more to demonstrate how Rust's whole set of features neatly fits together and that it would be hard to get the same guarantees with "just types".

replies(1): >>45049634 #
6. buzzin__ ◴[] No.45049625[source]
So, your argument is that Rust is better because better programers use Rust.

I specifically mean this part: "Rust would never have such a dumb library design".

One could then also say that Rust programmers would never make such a cyclical argument.

replies(1): >>45049640 #
7. johnfn ◴[] No.45049634[source]
I'm not as familiar with Rust, but isn't there still a gap? For instance, if we modified window.set_href to have move semantics, wouldn't this still work (i.e. not produce an error)?

    let win = window.set_href("/foo")
    win.set_href("/bar")
You might say "why would you ever do that" but my point is that if it's really the lack of move semantics that cause this problem (not the deferred update), then you should never be able to cause an issue if you get the types correct. And if you do have deferred updates, maybe you do want to do something after set_href, like send analytics in a finally() block.

In fact, Typescript does have a way to solve this problem - just make `setHref` return never[1]! Then subsequent calls to `setHref`, or in fact anything else at all, will be an error. If I understand correctly, this is similar to how `!` works in Rust.

So maybe TS is not so bad after all :)

[1]: https://www.typescriptlang.org/play/?ssl=9&ssc=1&pln=9&pc=2#...

replies(2): >>45049967 #>>45050736 #
8. johnfn ◴[] No.45049640[source]
If you want to be more charitable, you could say "Rust library design is superior to the Web API library design", and I'd say you were right - particularly for crufty stuff like .href which was designed decades ago.
9. dominicrose ◴[] No.45049709[source]
The Rust example was interesting but the Typescript example doesn't show if TS would or would not be good for a big project.

I'm scared of Ruby because I catch bugs at runtime all the time, but here's the thing: it ends up working before a commit and it was easy enough to get there and it's satisfying to read and edit the code. Now wether I can keep going like this if the project become bigger is the question.

The location.href issue is really a javascript problem that has been inherited by TS. Because JS allows to modify attributes, the browser kind of has to take the change into account. But it's not like Ruby's exit keyword. The page is still there until the next page loads and this makes total sense once you know it.

10. bkolobara ◴[] No.45049967{3}[source]
Your Rust example would not work, because `window.set_href("/foo")` would not return anything (it would return the unit type "()" aka void). And you can't call `set_href()` again on "()". This is a common pattern in Rust, allow certain functions to only be called once on specific objects.

I really like your TypeScript solution! This actually perfectly solves the issue. I just wish that this was the ONLY way to actually do it, so I would not have experience the issue in the first place.

replies(1): >>45050217 #
11. johnfn ◴[] No.45050217{4}[source]
Thanks for the explanation! That's a very nice pattern.

> I just wish that this was the ONLY way to actually do it

I completely agree.

12. mcherm ◴[] No.45050371{3}[source]
> If setting href worked like that, I think it would be less confusing.

How do you imagine this would interact with try-finally being used to clean up resources, release locks, close files, and so forth?

replies(1): >>45051543 #
13. norman784 ◴[] No.45050736{3}[source]
Just to add what the other comment said, take a look and run these two examples to see the errors you could get:

https://play.rust-lang.org/?version=stable&mode=debug&editio...

https://play.rust-lang.org/?version=stable&mode=debug&editio...

14. fleabitdev ◴[] No.45051543{4}[source]
try-finally is leaky in JavaScript, in any case. If your `try` block contains an `await` point, its finaliser may never run. The browser also has the right to stop running your tab’s process partway through a JavaScript callback without running any finalisers (for example, because the computer running the browser has been struck by lightning).

For this reason, try-finally is at best a tool for enforcing local invariants in your code. When a function like process.exit() completely retires the current JavaScript environment, there’s no harm in skipping `finally` blocks.

15. qalmakka ◴[] No.45052032[source]
Yeah, the problem is that some old web APIs have been clearly hacked up haphazardly in the '90s, probably in a hurry, and now we have to live with the consequences of that. This is not unique with the Web though, it's basically the same with the entirety of the WinAPI and most libc functions in my experience
16. socalgal2 ◴[] No.45052981[source]
That is not a repo of the code in the article. The article's code is effectively

    set_href('/foo');

    let future = doSomethingElse()
    block_on(future)

    if (some_condition) {
        set_href('/bar');
    }
This code makes the bug clearer. doSomethingElse is effectively allowing the page to exit. this would be no different in many apps, even in rust.

The browser does not start a process when you set `window.location.href`. It starts a process after your code exits and lets the event loop run other tasks. The `await` in the example code is what allow other tasks to run, including the task to load a new page, (or quit an app, etc..) That task that was added when you set `window.location.href`

If that's not clear

    // task 1
    window.location.href = '/foo' // task2 (queues task2 to load the page)

    let content = await response.json(); // adds task3 to load json
                                         // which will add task4
                                         // to continue when finished

    // task4
    if (content.onboardingDone) {
        window.location.href = "/dashboard";
    } else {
        window.location.href = "/onboarding";
    }
task2 runs after task1. task1 exits at the `await`. task2, clears out all the tasks. task3 and task4 never run.
replies(1): >>45062159 #
17. depressedpanda ◴[] No.45062159[source]
No, I think you misunderstand how it works. The problem is that task 4, as you call it, runs after the navigation triggered by the redirect value.

The the author expects the side-effect -- navigation to a new page -- of the window.location.href setter to abort the code running below it. This obviously won't happen because there is no return in the first if-statement.

replies(1): >>45077304 #
18. socalgal2 ◴[] No.45077304{3}[source]
There is a return, it's disguised as "await"

*simplified*, the symantics of "await" are just syntactic sugar

    const value = await someFunction()
    console.log(value);
is syntactic sugar for

    return someFunction().then(function(value) {
      // this gets executed after the return IF
      // something else didn't remove all events like
      // loading a new page
      console.log(value);
    });