[Roadmap 2017] Productivity: learning curve and expressiveness

There is an interesting argument to be had there, but it’s off topic for this thread. The productivity and learning curve isn’t even ideal for the programming styles that do work well. This is already hurting people who are using Rust today and those who would have a great time with Rust if the steep learning curve didn’t dissuade them. Even if I believed that mostly-pure functional programming (as opposed to the predominant mostly-imperative style with occasional closures thrown in) was a desirable and achievable style for Rust, it’s is a separate issue with entirely different challenges and potential solutions.

1 Like

In the end Haskellers need to learn to program with the idioms of Rust.

Can’t agree at all. Rust is close to being able to provide good support for many haskell/functional idioms, and I desperately hope that in a year, they will have a much more welcoming experience.

3 Likes

Idiomatic Rust code is pretty functional:

    for &coord in group.coords().iter() {
        let m = self.neighbours(coord).iter()
            .filter(|coord| self.color(coord) == enemy)
            .map(|&coord| self.get_chain(coord).unwrap().liberties())
            .filter(|ref libs| libs.len() == 1)
            .flat_map(|set| set.into_iter())
            .map(|coord| Play(player, coord.col,coord.row))
            .filter(|&play| self.is_legal(play).is_ok());

        solutions.extend(m);
    }

I just want a few extra Rust features for better support.

1 Like

Which IMHO pretty nicely shows the problems of functional programming. :wink:

Having multiple intermediate results - bound by let and named accordingly - makes the code a lot more readable.

1 Like

it’s perfectly readable because it’s on purpose not point-free

the names of the variables in the closure lets you follow the transformations:

first you select just the enemy coordinates then for each coord you get the liberties of its chain then you select the liberties where the aforementioned chain had exactly one then you make a set of those then you take the set of those coordinates and make it into plays then you select only the legal plays

add those legal plays to solutions

I could even write much more descriptive names like .map(|&enemy_coord| self.get_chain(enemy_coord).unwrap().liberties())

2 Likes

The problem with code like this is, that you've to read the whole transformation and can't start somewhere in between.

Intermediate results act a bit like mental stop points, where you can continue your reading. Without them you have to keep more context in your head.

Sure, people might consider different things more or less readable, but having to keep more context makes code in quite a lot of cases less readable, especially if you haven't written it yourself.

This in particular always struck me as odd and unnecessary. It seems like the complier should be able to infer the type of the variable from its eventual use as an argument, and flow that back to the closure definition.

4 Likes

I guess it’s a personal preference, but I like that sort of transformation chain that iopq provided as an example, more so than if the intermediate results had been named.

When the closures are short and obvious like that, and the closure arguments are given proper names as in the example, I really don’t think additional names for intermediates are adding anything for the reader.

To me the chaining also helps to see at a glance that each stage only depends on the output of the previous, and that no intermediate results are being used anywhere else in the remainder. If you have named intermediates and you want to make changes to various stages, you have to first make sure the existing intermediates aren’t being used in the rest of the function.

4 Likes

Concerning expressiveness of Rust, I had proposed an improvement for implementations and composition. Most of the people who commented expressed their enthusiasm for the idea but I've yet to receive feedback from the language team.

2 Likes

I’ve personally been thinking a lot about this RFC lately. I think some delegation mechanism is needed, but its sort of tricky. In addition to the ‘composition delegation’ concepts you cover in the RFC, there are also desires to enable delegation between trait impls and inherent impls. I’ve been meaning to leave a comment on the RFC thread, but I’m trying to figure out how to balance these different needs.

2 Likes

I have recently added possible extensions although they may not cover all the cases you're thinking about. Now I'd be very interested in hearing which scope you would like to cover, what are the tricky cases you have in mind and what is the impact on the possible manners to handle delegation.

The reasoning is tricky but it is a necessary hack if you have ordered type-checking: that is, while rustc employs HM type inference, it also has type-dependent resolution (things like fields and methods) and it traverses the function in AST order, so when it needs information, it can only know the results of HM inference for the parts of the function that come before in the source.

To solve this one must move to an order-independent constraint-based type-checking system: just like we have trait bounds checked in whatever order they happen to become known (i.e. enough inference information has been collected), we could have every single type-dependent construct kept around until it can be resolved.

This would be a significant rewrite, but strictly an improvement, if Rust didn’t have coercions.

The problem with coercions is they don’t primarily rely on HM inference (if you infer too deeply then you end up with a trait bound failing instead of a later coercion), although they do use it to understand e.g. function calls, their system is instead an “expected type hint” passed down from parent to child and transformed as needed.

This is the only kind of “eager top-down type propagation” rustc can do - the expected type reaches the leaves first, and only then HM can propagate what it might have learned from what was expected.

This hybrid system is problematic because of relatively determinism: how do you keep the same kinds of code working, i.e. how do you do out of order coercions withiut accidentally leaking some type further than they should reach (e.g. preventing a coercion in the only place it could be applied)?

Swift’s answer seems to be bactracking, and you can find discussions around (sadly I don’t have one on hand atm) about implicit type conversions resulting in potentially days of compilation time (backtracking being roughly of factorial time complexity while only linear space usage, so you can’t easily run out of memory, as I’ve had the recent unfortune of finding).

The most promising solution IMO is a boring fixed-point algorithm that repeatedly tries to resolve constraints it can be certain of, and when it runs out of those, just picks the first (in AST order) coercion to resolve to identity, which will hopefully unlock other constraints, without doing so in a way that would be backwards-incompatible.

This might be something we pursue in 2017 but not at the cost of compiler performance.

4 Likes

Re the fixed-point algorithm. That sounds quite a bit like how I’ve structured type checking and inference in mrustc

It works rather well, but has required a few patches here and there to properly cover all of the code rustc can check. From my rough testing with some cases that people on #rust have complained about, it manages to correctly infer some cases that rustc fails on. A disclaimer - This has only been tested by compiling libstd (and all dependencies), so there may be places where it falls flat on its face.

A rough layout of how it works:

  • An initial pass over the function’s AST to both give IDs to all unknown types (_) and build up a set of known equalities (applied using HM type inference), coercion equalities (stored for later checking), and trait bounds (including operator overloads)
  • Iterate until there’s no changes in the ivar set:
  • Run code to handle method lookup, indexing, value calls, … (anything that needs auto-deref)
  • Check trait bound rules (which can provide the type of an associated type)
  • Check coercion points (recording the possibilities for ivars, applying type equality if a coercion is impossible, or inserting the coercion operation if required)
  • If nothing changed above, Use type possibilities from coercion rules to propagate type information through coercion points.
  • If nothing changed above, replace literal types with the defaults (i32 or f64)
  • Check that there are no rules left, and that there are no unknown ivars left.

(For the brave, see src/hir_typeck/expr_cs.cpp)

2 Likes

(@thepowersgang the link has a typo)

waves hand no it doesn’t (Fixed, thanks)

2 Likes

I agree, it would help recognizibility if the most important part of the signatures were highlighted, for example using a bold font, instead of the least important parts being hidden.

Language usability has recently improved a bit, becoming a little more DRY. This is the old version of Rust code:

struct MyStruct { data: u32 }
impl MyStruct {
    fn new(data: u32) -> MyStruct {
        MyStruct { data: data }
    }
}
fn main() {}

With the latest Nightly you can write it like this:

#![feature(field_init_shorthand)]

struct MyStruct { data: u32 }
impl MyStruct {
    fn new(data: u32) -> Self {
        Self { data }
    }
}
fn main() {}
5 Likes

Could we auto-derive a constructor method for structs that just takes all contents as arguments – basically #[derive(new)]?

3 Likes

You should write an enhancement request for this nice idea :slight_smile: