Live data from Hacker News

Rust's Block Pattern

notgull.net

101–110 of 121 posts

Re: Rust's Block Pattern

#101
post #61

Earlier quoted context omitted.

Each does different things, and Rust also has plenty of them. and_then(), or(), or_else(), then(), the list goes on. Kotlin just implements them more widely. Actually, Kotlin's with() and apply() are more powerful than what Rust can provide. Then again, Rust isn't designed with OO in mind, so you probably shouldn't use those patterns in Rust anyway.

I think you've misunderstood the point they were making by addressing the number as if it was the only concern and then only mentioning the actual point they were trying to make as if it were an incidental afterthought. I don't think it's likely they're criticizing five functions in the standard library is too many, but that having five special functions with certain semantics that only apply to them is too many. The…

The Kotlin functions are actually quite easy to write, they're all written in standard Kotlin.

also: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std...

apply: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std...

let: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std...

with: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std...

run (two overloads): https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std... and https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std...

These all heavily rely on Kotlin's ability to write an extension function for any class. When you write `with(x) { something() }` you're extending the type of `x` (be that int, List, or SomeObject) with an anonymous method, and passing that as a second parameter.

Consider the signature here:

    public inline fun  with(receiver: T, block: T.() -> R): R
The first object is a generic object T, which can be anything. The second is a member function of T that returns R, which again can be just about anything, as long as it operates on T and returns R.

Let does it kind of diferently:

    public inline fun  T.let(block: (T) -> R): R
This is an extension method that applies to every single class as T isn't restricted, so as long as this function is in scope (it's in the standard library so it will be), every single object will have a let() method. The only parameter, block, is a lambda that takes T and returns R.

So for instance:

   val x = makeFoo()
   with (x) {
      bar = 4
   }
is syntactic sugar for something like:

   fun Foo.anonymous() {
      this.bar = 4
   }

   val x = makeFoo()
   with(x, Foo::anonymous)

You could absolutely write any of these yourself. For instance, consider this quick example I threw together: https://pl.kotl.in/S-pHgvxlX

The type inference is doing a lot of heavy lifting, i.e. taking a lambda and automatically turning it into an anonymous extension function, but it's nothing that you cannot do yourself. In fact, a wide range of libraries write what might look like macros in Kotlin by leveraging this and the fact you can define your own inline operators (i.e. https://pl.kotl.in/TZB0zA1Jr).

This isn't possible in many other languages because taking a generic type definition and letting it possibly apply to every single existing type is not exactly popular. Combined with Kotlin's ability to extend nullable types (i.e. this = null) as well makes for a language system that wouldn't work in many other flexible languages.

Re: Rust's Block Pattern

#102
post #86
post #48

Earlier quoted context omitted.

I wouldn't call Rust "a big language" because of labeled break. This is a pretty standard language feature, you can do the same in C (and therefore C++), Go, Javascript, Java, C#...

Those languages don't treat blocks as expressions, so you really can't do the same thing there. Something very similar, yes. But not the same.

Those languages aren't expression-oriented, so you would need to assign the result to a previously-initialized variable in a higher scope. But that just makes this pattern clunkier in those languages. This subthread is about jumping to labels, which is a relatively obscure yet widespread feature supported by many languages (though C and Go allow forward jumps, and the rest only allow backward jumps, since the latter ensures that control flow does not become irreducible).

Re: Rust's Block Pattern

#103

You can also de-mut-ify a variable by simply shadowing it with an immutable version of itself: let mut data = foo(); data.mutate(); let data = data; May be preferable for short snippets where adding braces, the yielded expression, and indentation is more noise than it's worth.

Variable shadowing felt wrong for a while because it's considered verboten in so many other environments. I use it fairly liberally in rust now.

It helps that the specific pattern of redeclaring a variable just to change its mutability for the remainder of its scope is about the least objectionable use of shadowing possible.

Re: Rust's Block Pattern

#104

Earlier quoted context omitted.

I just learned this one, and am gradually starting to use it! It applies for loops too. I saw it in ChatGPT code, and had to stop and look it up. Rust is a big language, for worse and for better.

break 'label value; ... is something to be used very sparingly. I reckon I write a new one about once a year. Very often if you think harder you realise you didn't want this, you should write say, a function (from which you can return) or actually you didn't want to break early at all. Not always, but often. If you write more "break 'label value" than just break then you are almost certainly Doing It Wrong™.

Not having put it into practice yet, there is a pattern I use regularly which I plan to replace with the labeled one: I set a flag at the top of the loop I have an inner loop. The inner loop can set this flag. Directly past the inner loop, I check for the flag, then break. I am pretty sure this is exactly what the labeled break is for.

Re: Rust's Block Pattern

#105
post #16

This is one of those natural consequences of "everything is an expression" languages that I really like! I like more explicit syntax like Zig's labelled blocks, but any of these are cool. Try this out, you can actually (technically) assign a variable to `continue` like: let x = continue; Funnily enough, one of the few things that are definitely always a statement are `let` statements! Except, you also have `let` expr…

I'm not sure why you picked continue here? All the diverging control flow instructions have the same type, ! aka "Never". In stable Rust you're not allowed to use its name but it's "just" an empty type and you can easily make one of those yourself - an enum with no variants.

Re: Rust's Block Pattern

#106
post #51

Earlier quoted context omitted.

Can this just be done as a lambda that is immediately evaluated? It's just much more verbose. let x = (|| -> Result { Ok("1".parse:: ()? + "2".parse:: ()? + "3".parse:: ()?) })();

Wouldn't that also move any referenced variables too? Unlike the block example that would make this code not identical to what it's replacing.

No, unless you ask for it via the `move` keyword in front of the closure.

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

Re: Rust's Block Pattern

#107
post #94

Earlier quoted context omitted.

It sounds like you're fighting the language - Rust is sort of FP-light and you're encouraged to return a null/error value from the intermediate calculation instead of doing an early return from the outer scope. It's a nice and easy to follow way to structure the code IME. Yes, it's more verbose when an early return would have been just right - so be it.

For the case where `try` is useful over the functional form (i.e. parent's situation of having a desired Result, plus some unrelated early-returning), that ends up with nested `Result`s though, i.e. spamming an `Ok(Ok(x))` on all the non-erroring cases, which gets ugly fast.

Why couldnt you flatten it?

Re: Rust's Block Pattern

#108

Earlier quoted context omitted.

Not sure if that is relevant to your point, but: For better and for worse, closing over any outer scope variables is syntactically free in Rust lambdas. You just access them.

It's syntactically free, but it can cause borrow-checker errors thst cause your code to outright fail to compile.

Yes, exactly. My concerns were semantic, not syntactic.

Re: Rust's Block Pattern

#109
post #61

Earlier quoted context omitted.

I think you've misunderstood the point they were making by addressing the number as if it was the only concern and then only mentioning the actual point they were trying to make as if it were an incidental afterthought. I don't think it's likely they're criticizing five functions in the standard library is too many, but that having five special functions with certain semantics that only apply to them is too many. The…

The Kotlin functions are actually quite easy to write, they're all written in standard Kotlin. also: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std... apply: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std... let: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std... with: https://github.com/JetBrains/kotlin/blob/2.3.0/libraries/std... run (two overloads): https://github.com/JetBr…

Fair enough, I retract my previous comment. Unfortunately there seem to a lot of pieces that are unfamiliar here so I'm not really able to understand parts of this but I trust that you understood what I was saying well enough to know that it was wrong.
Post reply on HN