Some Scala warts (and Rust?)


#1

A nice partial list of nonwarts/warts in Scala language:

http://www.lihaoyi.com/post/WartsoftheScalaProgrammingLanguage.html

And some comments:

I’d like to read a similar long article about nonwarts/warts of Rust :slight_smile: Do you have some entries for such article?


#2

The only one I can think of myself would be borrow checker sometimes just not being flexible enough. This affects many parts of language (requiring weird workarounds, some extra variables or blocks), but I will provide a single example.

For instance, Rust may complain about immutable borrow stopping mutable borrow after immutable borrow shouldn’t apply. For instance, here, there is no way for arr.len() to happen after getting mutable reference to array element.

fn main() {
    let mut arr = [1, 2, 3];
    arr[arr.len() - 1] = 4;
}

Oddly enough, this is allowed.

fn main() {
    let mut a = 1;
    a += a;
}

But this isn’t.

use std::ops::AddAssign;

fn main() {
    let mut a = 1;
    a.add_assign(a);
}

#3

I can’t help thinking that in trying to keep the closure capture syntax as simple as possible, Rust actually ended up making closure capture even more complex than in C++.

It’s harder to understand for beginners due to the relatively high amount of implicit black magic (“why does it move here, but not there?”), while experienced users end up going through wordy binding-based workarounds due to the lack of an explicit capture syntax.

It works well most of the time, but when it doesn’t, it’s a bit of a pain to work with.


#4

And I reminded myself of something. Trait coherence. You cannot write code that is generic over Self when the trait isn’t from the the crate that declared it, so you cannot say overload 2 + X(2).

Such trait implementation can be used for instance creating a matrix implementation, where something like 2.0 * matrix is expected to work. With complex numbers, it’s reasonable to write 3 + 4*i.

use std::ops::Add;

struct X<T>(T);

impl<T> Add<X<T>> for T
    where T: Add<Output = T>
{
    type Output = T;
    fn add(self, other: X<T>) -> T {
        self + other.0
    }
}

You can however substitute T for any given type, and that will work, but that’s clearly a workaround, and won’t be generic.

use std::ops::Add;

struct X<T>(T);

impl Add<X<f64>> for f64 {
    type Output = f64;
    fn add(self, other: X<f64>) -> f64 {
        self + other.0
    }
}

#5

I have some pet peeves:

  • You can export pub extern "C" fn foo() {} from your crate, but you still won’t be able to call it from C as foo(). Rust defaults to exporting public C-compatible functions with Rust-private C-incompatible names.

  • You can’t index arrays with u32 or anything else than usize. If you insist on saving space on your indices (or need compatibility with C int), the code will be littered with unchecked casts.

But it’s not too bad overall. Most of annoyances are on Rust’s todo list (I can’t wait for non-lexical lifetimes, consts in traits that will make arrays usable, etc.)

Modules were really hard for me to get (absolute vs relative paths, docs explaining using mod{}, not files), but once I’ve learned them I don’t think that’s a wart any more.


#6

Hm, can you give an example of this? I’ve been using Rust for a while, and closures always “just” worked for me. I always wondered how is it possible that in C++ you can decide to move/copy/reference each binding separately, while in Rust you can’t and don’t seem to need to.

For me personally the three biggest problems are:

  • paths in use items don’t need :: to be global,
  • lifetime elision sometimes backfires and makes your function to compile by itself, but fail at call side (fn foo(&'a self, &'b X) -> &'b Y is the common case which gets wrong lifetimes with elision),
  • built-in Error trait just feels wrong to me (I can’t put my finger exactly on what I don’t like, but implementing Error without error_chain is a pain, and even with error-chain you sometimes seem to need strange contortions).

#7

I’m not a long-time Rust user, but when I’ve seen it for the first time I thought - wow, who design new programming language with ;?!
Also agree with matklad, that error handling story feels like something went wrong :slight_smile: Too much boilerplate.
I’ve not tried to deep dive into macro-coding yet, but at first glance macro-system seem to be not as powerful as D’s and it does not seem to be easily improved.
Incremental compilation…


#8

Lack of tuple assignment


#9

I don’t see how that can happen, do you have an example? Surely if you are borrowing from X but claiming you borrow from self the compiler would error at the function definition?


#10

Yep, I am wrong here, this particular pattern is unlikely to compile at function definition site :slight_smile:

I should have found the actual instance of the problem, rather then inventing it on the spot. So, here’s this problem in one of my projects, where I had to “fix” function definition, although it used to compile just fine, until I’ve called it: https://github.com/matklad/fall/commit/73eeeb9d5f464fbdb2bb7b8f24975f4d8f586bb9#diff-036076241b912ab646c1ef74b94afccaL31

And here is a gist of the problem: http://play.integer32.com/?gist=3aba0526aff729bb9d62483a10732826&version=undefined

EDIT: Hm, I wonder if we can lint against it?

EDIT: Idea of a warning: https://github.com/rust-lang/rust/issues/42287


#11

Rust defaults to exporting public C-compatible functions with Rust-private C-incompatible names.

To be fair, when you write pub extern fn foo() {}, it is scoped to that module. Otherwise, it would be very easy for multiple crates/modules to accidentally define the same pub extern fn foo().


#12

Actually, there are reasons why you would want a C calling convention function with name mangling. extern "C" simply says “C calling convention”. For instance, if you want to provide a callback function to C function (for instance qsort), it needs to have C calling convention. You don’t care if a function can be named from C, all you care is whether function can be called from C side when having a pointer to it. Essentially such a function works like static functions in C.

Still, it’s somewhat rare of a case.


#13

implementing Error without error_chain is a pain
Has it ever been considered to add a simple concrete error type, std::error::StringError? To complete the picture, also an alias std::boxed::Result to represent the very convenient Result<T,Box<Error>>. The bail! macro as implemented in simple_error would be useful as well.


#14

I believe it’s called Box<Error>.

use std::error::Error;
use std::fs::File;

fn example() -> Result<(), Box<Error>> {
    File::open("hello")?;
    // bail
    Err("Oh well")?
}

#15

Sure, Box<Error> is fantastic. But it’s not a simple concrete error type that you can create and then box. Which is why we have the simple_error crate. Just thought that a concrete type wrapping a String would be a useful addition to the stdlib.


#16

The main issue with Rust’s closure capture mechanism is that it does a lot of implicit black magic behind the scenes. The compiler is given very little information, and will thus need to try its best to deduce a closure capture mode which makes sense given the context of the code. This causes two different classes of problems.

First, the user can accidentally capture too much in the closure without noticing, leading to ownership errors (e.g. capturing a &mut or moving when you wanted/expected a &). These tend to manifest themselves only at a later point in the development cycle, when the variable is needed again in the same function, leading to hard-to-understand borrow checker errors.

This could not happen if capture was explicit. Effectively, if you’re familiar with OpenMP, it’s a close cousin to the beginner mistake of forgetting that variables default to shared, an truly unfortunate choice which led generations of OpenMP educators to start their classes by teaching their students to disable this feature with default(none).

The second problem with Rust’s capture black magic is that the compiler sometimes knows too little about the code’s context to deduce a useful capture mode. For example, closures used as part of thread::spawn or an iterator-based expression will usually fail to capture the surrounding state in a useful way, in turn requiring the use of “move”.

The problem with move is that it is an ownership sledgehammer which tries to swallow every single piece of state into the closure, even when a reference would have sufficed. So as soon as you go with “move”, you end up having to work around its excessively greedy behaviour by defining lots of bindings to references or putting variables in Rc or Arc when you shouldn’t need to…

As I said, it works most of the time, but when it doesn’t, it’s really a mess.


#17

Here’s another gotcha that arguably could be called a wart: https://www.reddit.com/r/rust/comments/6dynjz/psa_temporaries_in_final_expression_in_block_live/