Hacker News new | ask | show | jobs
by ragnese 2123 days ago
I realized after I wrote the comment that I was really referring to the closure traits when I said that. And I really should have said "kind" instead of "type" because, like you said, every different function has its own type.

But anyway, I don't really disagree with your point about categorizing languages as OOP, Procedural, or Functional.

But honestly, in this case, I think it's pretty damn clear than Rust is procedural WAY more than it's either OOP or FP. (Note: By OOP, I mean Java-style with tall ownership hierarchies and object-managed mutable state, not necessarily caring about inheritance. And definitely not referring to Alan-Kay-Style-OOP a la Lisp and Smalltalk).

Scala can be looked at as FP and/or OOP. C++ can be looked at as Proc and/or OOP. Python, IIRC, can kind of do all of them, but I don't remember it being easy to make copies/clones in Python, so FP is questionable.

Have you ever tried to write two versions of a complex async function in Rust? One with async and one with Futures combinators? Due to ownership, the Futures combinators approach very quickly devolves into a nightmare. The language doesn't "want" you to do that.

What about function composition? Very awkward to do with matching up the different Fn traits.

And deeply nested object hierarchies are a no-go, too, because of the inability to do "partial borrows" of just a single field of a struct.

I mean, yes, it's not C because it has a real type system and generics. But... it's pretty much C in that you just write functions and procedures that operate on structs.

EDIT: Perhaps my "hardline" approach on calling Rust procedural is in response to people who have come to Rust from non-FP languages, see `map`, `filter`, and `Option` and start calling Rust functional. That's not functional programming! Ask someone who does OCaml to try out Rust and see if they call Rust functional afterwards.

2 comments

>And deeply nested object hierarchies are a no-go, too, because of the inability to do "partial borrows" of just a single field of a struct.

I'm not totally sure if this is what you mean, but FYI you can borrow multiple fields mutably using destructuring:

    struct Foo(u8, u8);
    
    fn main() {
        let mut bar = Foo(1, 2);
        let Foo(ref mut x, ref mut y) = &mut bar;
        *x += 1;
        *y -= 1;
        println!("{} {}", bar.x, bar.y);
    }
The problem comes when someone else needs to also borrow the Foo, even immutably. In Java-style OOP, you typically have "objects" that own other objects, all the way down. And you manage state internally.

So it often comes up that you might call several methods in a given scope. If even one of those mutably borrows one field of one sub-object, then you can't have any other borrows of that object anywhere else in that scope.

Newbies from other languages trip on that often enough that I used to see questions about it in r/rust fairly frequently.

Just let x = &mut bar.0 will work, but this "intelligence" is confined to the body of a single function. Rust possesses the somewhat curious property that there are functions that are intended to always be called like this

  foo(&bar.x, &bar.y, &bar.z)
which cannot be refactored to

  foo(&bar)
This is a complex topic, but what it boils down to is that the function signature is the API. If you borrow the whole thing, you borrow the whole thing, not disjoint parts of it.

This is also why it's okay in the body of a single function; that doesn't impact a boundary.

We'll see what happens in the future.

My preferred solution to this is to make partial borrows part of the method definition syntax to make it clear this is part of the external contract of your API. Also I lean towards mimicking the arbitrary self type syntax and land on something along the lines of

  impl Foo {
      fn bar(self: Foo { ref a, mut ref b, .. }) {}
  }
Where that signature tells the borrow checker that those two fields are the only ones being accessed. Nowadays this method would have to be &mut self, which heavily restrict more complex compositions, as mentioned in this thread.
On one hand this looks/feels absolutely useful and kind of "must have" feature, yet the boilerplate seems excessive. But at the same time it's absolutely something that should be described somewhere as metadata for a function. It seems to me this should be something that the compiler figures out and mostly elides, like lifetimes. (But this should be present as part of API, should be easily discoverable, should be there at compile time when linking against crates, etc.)
The problem with having the compiler figure it out is that the signature now becomes dependent on implementation details that can't be seen from the signature, and could be accidentally changed. This information must be an explicit part of the signature, so that it's properly visible and spells out what the function can do without having to read the body.

That said, I think putting it in the argument list like that is a terrible idea. It would add far too much clutter. What if it was put into the where section, sort of like this:

  impl Foo {
      fn bar(&mut self, other_param: &Data)
        where self: borrows{ a, b, .. },
              other_param: borrows{ a, b, ..},
      {
        // Function body
      }
  }
In one sense, it sort of fits because it's providing a "bounds" of sorts on how the reference can be used, similar to the way a trait bound would for a type. If no reference binding is provided it would default to borrowing all the fields, which is the current behaviour.
But honestly, in this case, I think it's pretty damn clear than Rust is procedural WAY more than it's either OOP or FP. (Note: By OOP, I mean Java-style with tall ownership hierarchies and object-managed mutable state, not necessarily caring about inheritance. And definitely not referring to Alan-Kay-Style-OOP a la Lisp and Smalltalk).

Scala can be looked at ...

Interesting perspectives, and I largely agree with all of them.

Related: I heard someone else say that while Clojure and Erlang embrace immutability for concurrency, Rust shows that you can "just mutate". It's still safe for concurrency (due to its type system).

Rust seems to be one of the only languages that embraces the combination of algebraic data types + stateful/procedural code.

But I've also found this in my Oil project [1], which is written with a bunch of custom DSLs!

I wrote it in statically-typed Python + ASDL [2], so it's very must procedural code + algebraic data types. Despite historically using an immutable style in Python, this combo has grown on me. Lexing and parsing are inherently stateful, and use a lot of mutation.

----

On top of that, my collection of DSLs even translates nicely to C++. Surprisingly, it has some advantages over Rust! The model of algebraic data types is richer ("first class variants"), described here:

https://news.ycombinator.com/item?id=24136949

https://lobste.rs/s/77nu3d/oil_s_parser_is_160x_200x_faster_...

[1] https://www.oilshell.org/

[2] http://www.oilshell.org/blog/tags.html?tag=ASDL#ASDL

> Related: I heard someone else say that while Clojure and Erlang embrace immutability for concurrency, Rust shows that you can "just mutate". It's still safe for concurrency (due to its type system).

Yes! I will repeat a sentiment I articulated on Reddit about that. Even after having used Rust on a handful of small-to-medium sized projects since 2016, I never realized that I could loosen/abandon my immutability fetish that I had been trained to love over the years of working with C++ and Java. C++ needs it for concurrency, and Java needs it for concurrency and because every method can mutate its inputs without telling you. Rust doesn't have either of those problems. Having immutable-by-definition objects in Rust isn't really that useful (unless, of course, the thing is naturally, semantically, immutable anyway, like a Date IMO). It was an eye-opening epiphany and I'm excited for my next Rust session to see how my "new worldview" pans out. :)

Yes I'll be interested to see how it turns out. Any blog posts / writing on the procedural viewpoint will be appreciated.

Does Rust have something like C++'s const methods? Where you can have a method that mutates a member, but doesn't logically mutate from the caller's perspective?

It seems like you could be prevented from having races on individual variables, but still have races at a higher level.

Like on database cells. I guess no language will help you with that, and that's why Hickey wrote Datomic -- to remove mutability from the database.

> Does Rust have something like C++'s const methods? Where you can have a method that mutates a member, but doesn't logically mutate from the caller's perspective?

Yes! "Interior mutatbility" is the term to search for. In Rust, you'd wrap the field in a RefCell<T>. Many connection-pool implementations use interior mutability to manage the connections transparently to the caller.

Interior mutability is basically what, e.g. OCaml, does by default. In Rust, it's opt-in.

Yeah, DB ops are always a sticking point for figuring out how to write my APIs.