Hacker News new | ask | show | jobs
by 0xe2-0x9a-0x9b 3942 days ago
There is nothing in Rust that would ease solving hard problems. Graphs are unsupported. No transactional memory. No distributed computing. No migration. No support for trying. No memoization. No proofs.
8 comments

Depends on your definition of a hard problem. Most systems programming requires none of these things.

It may come as a surprise to some but most systems engineering has very little hard computer science involved and is mostly about achieving very simple tasks in reliable ways with very robust error handling.

You rarely implement novel data-structures or algorithms. Not shipping with cyclic data-structures in Rust is not really a problem for most systems applications. That said, you actually -can- build them in Rust, and it's likely there will be nice libraries with various different allocation/memory management and other runtime tunables and performance tradeoffs.

If you exclusively consider hard computer science problems as "hard problems" then no, Rust is probably not for you. Consider Haskell or Julia.

One might argue that "guaranteeing your web browser doesn't have memory leaks" is a "hard problem." Ergo https://github.com/servo/servo . I agree, though, that the problems you mention are all ones where I'd love to see a greenfield programming language with as much attention to detail and support as Rust has.
(Rust doesn't guarantee an absence of leaks, actually. Just memory safety.)
It's still really hard to leak in Rust though. Affine types and all.
In what conditions could Rust code leak memory, excluding the use of unsafe code? What are the technical reasons you can't guarantee an absence of leaks?

  >  What are the technical reasons you can't guarantee an absence of leaks?
Well, 'leak' is one of those things that's easy for a programmer to understand, but a hard thing for a computer to understand, because it's really about intent. How long did you intend for some resource to live? Any global value is, in some sense, a leak. We had a long discussion about this, and, at least currently, we couldn't come up with a formal enough definition of 'leak' to even start tackling the problem of "how do we solve leaks." (It is entirely possible that I am unaware about research on this topic... but given that solving leaks wasn't a goal of Rust, fixing it would just be gravy anyway. You have to choose your battles, and Rust certainly isn't perfect.)

As for how safe Rust can leak:

    let x = Box::new(5);
    std::mem::forget(x);
which can itself just be implemented in safe code:

    use std::cell::RefCell;
    use std::rc::Rc;

    fn forget<T>(val: T) {
        struct Foo<T>(T, RefCell<Option<Rc<Foo<T>>>>);
        let x = Rc::new(Foo(val, RefCell::new(None)));
        *x.1.borrow_mut() = Some(x.clone());
    }
or something like "I have a thread that is holding the receiving end of a channel that infinitely loops without reading anything off," in which case anything sent down that channel leaks.
That's a very intriguing block of code there. I'd like to think that while Rust may not have been designed to guarantee that leaks are prevented, it so happens that anything that could leak memory sticks out like a sore thumb in code review, since the code to get something to leak needs to be explicit. I'd consider a global variable or a channel that has the possibility of not reading all its values to follow that pattern. Whereas it's much easier to get something "stuck" in memory without a reference in C++. But you're absolutely right that memory safety does not at all imply "leak" safety, nor is the latter well defined at all!
Cycles of reference counted objects.

It is technically possible to guarantee the absence of leaks by introducing a `?Leak` trait to the language. There was a point in time when many wanted Rust to go that way, but it was ultimately decided against (it complicates other things).

Note that the trait-based scheme referenced here would only prohibit specific kinds of memory leaks. "Leaking", in its broadest usage, isn't a solvable problem in a Turing-complete language since an infinite loop can be considered a leak.
Interesting. How does Rust prevent the cyclic reference counting issues that led Chrome to adopt Oilpan?
In Servo, DOM nodes are Rust values managed entirely by the SpiderMonkey GC, and clever (and evolving) techniques are used to teach Rust to integrate reliably with SpiderMonkey. Rust has the flexibility (or will) to safely integrate external GC systems.
do you know enough about low-level computer architecture to speculate if mild-to-moderate changes would be warranted in things like, e.g., cache sizes on CPUs? Branch prediction was implemented on generalizations drawn in research papers analyzing procedural codes. I'm wondering if the concurrency abstraction will add a significant working-set to the runtime that adversely impacts existing prediction algorithms. For example, predicting in a for loop is one thing, but when the target of the branch is always some new portion of memory (graphic, text, etc), it'll always be a cache miss, and there'll never be branch history for it-- because neither have touched it yet.

this is probably a silly question because the content, though it might always be in RAM, will be branched to an order-of-magnitude less than it'll be used (once it's been loaded to cache)...but still, there should be an observable temporal-boundary to the context stored in cache and branch predictor in a procedural language, that would be manifest as an abstraction-boundary in a more parallelized environment

I don't really understand what you're trying to get at, but the concurrency abstractions/safety in Rust is static, there's little-to-no dynamic cost over the raw C/C++ APIs. And, in fact, the checks mean that one can sometimes be more aggressive about what designs can be used, without risking weird runtime corruption.
Rust is a way better replacement for C/C++. If you didn't need those then you don't need Rust. It will make your software faster while not exploding in your face like C/C++, but it's not meant for you. There are other languages solving the kind of problems you care about. See: Erlang, Haskell, Idris/Agda, Scala.

And frankly: Your definition of "hard" is a bit restrictive.

> while not exploding in your face like C

not a fan of C++ but I personally think having to wade through the building blocks of C was an immensely valuable reverse turing test (ability of human to exhibit human-like behavior? apparently this term is already taken but you get my idea) and I bemoan the future where everyone is protected from their mistakes. How else did one build motivation to do it right? The gamble of a mistake is much more fun than of a compiler error.

What a strange objection. In Rust, your mistakes are discovered at compilation time. In C++, your mistakes are discovered when your code blows up in your face at runtime (and cross your fingers that the failure is deterministic). Either way the programmer still making mistakes, but one of these failure modes is clearly preferable.
yes. I'm suggesting the capacity trained and required to write safe-C is the same capacity applied to, e.g., higher level functions of your mind-- such as conceiving of, not developing, but conceiving of some of the performant optimizations in Rust in the first place.

Like a fertilizer that stimulates growth while young but a crutch which stunts advancement in old age.

Well then you'll be happy to hear that writing correct `unsafe` blocks in Rust requires just as much care as writing correct C code. :)
> No transactional memory. No distributed computing. No migration

??? These would be super easy to build on top of Send/Sync and lifetime tracking.

You might be confusing Rust the language with a hypothetical Rust/OTP or Rust/HOL or something.

The primitives to build a Rust/OTP aren't there (specifically: blessed green threads or some kind of blessed event system + reactor, for better or worse)

Without those, we're going to see a hundred different solutions crop up, none of them being interoperable.

mio is doing pretty well as a most-popular event system and reactor. I wouldn't be surprised if it's blessed in the future, but at the moment the community seems to be there.

I'm also sort of hopeful that higher-kinded types can do something about writing code that can run on any such system. If you can have some functions on an abstract Promise<T> type, like fn then(Promise<T>, fn(T) -> Promise<U>) -> Promise<U>, it doesn't super matter which backend you're using. (As I understand it, JS is already here, because they can duck-type their promise spec.)

Also, less space than a Nomad. Lame!
I'm not sure what you mean by "Graphs are unsupported," exactly, but in a language as low-level as Rust, all that other stuff are library features, not language features.
> proofs

do I need to have functional experience for exposure to that? Only thing you listed that I haven't heard of.

I assume the comparison is to environments like Coq, Agda, Isabelle/HOL, Idris, etc. But in most of those environments, there's still a distinction between the language itself and the proof assistant.

I would not be shocked if someone figured out a brilliant way to add dependent types to Rust 1.x within the next few years.

proofs are different from unit testing, right?
Yes. For instance, say you're implementing a red-black tree. In case your algorithms are as rusty as mine are: it's a particular form of binary tree where every node has a "color", red or black, the children of any red node are black, all leaves are black, and the count of black nodes from the root to any leaf (aka the "black-height") is the same. The intention is that the tree is roughly balanced by following these rules: the minimum height is the black-height (all black nodes), and the maximum height is double the black-height (alternating red and black).

You might want to check that the property "the number of black nodes from the root to any leaf is the same" is always valid. With unit testing, you'd implement the insertion and deletion functions, and write a unit test where you give them a few trees and make sure that the insertion and deletion functions don't break that property, probably by looping over every leaf and comparing the black-height. If this passes, you know that your functions are probably correct and at least not obviously broken, but you don't know that there isn't some edge case you haven't thought of.

With a language that supports proofs, you'd tell the compiler about the concept of black-height, and the type of a red-black tree would include the black-height. Therefore, in order to construct a valid red-black tree, you have to have constant black-height. Just as much as you couldn't construct a red node with a red child, you can't construct a node with two children of varying black-height. When you say that your insertion function returns a red-black tree, as part of type-checking, the compiler makes sure that these properties hold -- because in turn, every helper function you call, every constructor, etc. also requires these properties to hold so that they can return an object of type red-black tree.

The downside is that it's much more involved than a few randomly-selected test cases to convince the compiler that you're upholding the red-black tree rules in every single case. This textbook has an example of doing this in the Coq proof assistant (search for the section labeled "Dependently Typed Red-Black Trees"):

http://adam.chlipala.net/cpdt/html/MoreDep.html