3

In functional programming languages, the most primitive/basic operation on a collection is the homomorphism map; it is (roughly) Collection[A] -> (A->B) -> Collection[B]

The Rust collections don't seem to support this. I suppose that's because they are mutable collections; if you're already using mutable collections an in-place update is more efficient.

Is there a separate "immutable collections" library (like with Scala) that I missed?

What about an "in-place map" operation that uses an A->B to mutate a Collection[A] into a Collection[B] (unlike ML and Haskell it's actually possible do to this safely because of affine types!) Or even the special case where A=B and the in-place map takes an A->A?

It's hard to use search engines to answer this question because all the hits involve "map" the noun (as in HashMap).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
user4718
  • 100
  • 10
  • We have powerful iterators and `collect()`, for all your FP need. What do you mean by inplace map? – Tatsuyuki Ishi Apr 29 '17 at 06:29
  • 1
    @TatsuyukiIshi: In Haskell, you can just `fmap` and keep the structure. For example, if you have a hash table, none of the keys have to be rehashed if you `fmap` to change the values. – Dietrich Epp Apr 29 '17 at 06:37
  • @TatsuyukiIshi: I did not realize that I should be looking in the iterator for the map function. Thanks! – user4718 Apr 29 '17 at 07:12

2 Answers2

4

Rust has a map() function, but it's not part of every individual container, but rather of the trait Iterator (Rust traits are very similar to Haskell's type classes): Iterator:map(). The trait contains many more useful methods, many of which should sound familiar to FP programmers.

Let's see map() in action:

let result: Vec<_> = vec![2, 3, 5, 7]
    .into_iter()      // we have to get an iterator over the vector 
    .map(|i| i * i)   // next we map each element
    .collect();       // finally we collect all elements into a new vector

The type of map() is what you would expect:

:: Iterator a -> (a -> b) -> Iterator b

Or in Rust syntax:

trait Iterator {
    type Item;
    fn map<B, F>(self, f: F) -> Map<Self, F> 
        where F: FnMut(Self::Item) -> B;
}

Which first looks way more complicated, but it actually makes sense and it will probably be better in the future. The signature is (self, f: F) -> Map<Self, F>. And:

  • self is an Iterator over Self::Item [compare: Iterator a]
  • F is an FnMut(Self::Item) -> B [compare: (a -> b)]
  • Map<Self, F> is an Iterator over whatever F returns (B) [compare Iterator b]

If you want to do an in-place a -> a map (meaning: without chaning the type), you can just obtain a mutable reference to each element and change it. Example:

let mut v = vec![2, 3, 5, 7];
for e in &mut v {
    e *= 2;
}

let mut m = HashMap::new();
m.insert("anna", 5);
m.insert("peter", 3);
for v in m.values_mut() {
    v *= 2;
}

Doing an in-place map that changes the type is currently not possible without unsafe code. In part because Rust's type system can't compare the sizes of two types at compile time (but it will change).

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Wow @Lukas, thank you for the super-detailed reply! Please don't think me ungrateful for having previously accepted Tatsuyuki's answer. In the end, all I was missing was the fact that, in Rustland, map is something you do to iterators rather than to the collections they come from. Thank you!! – user4718 Apr 29 '17 at 17:59
  • Also thanks for the comment about Traits being like Haskell type classes. I had figured that out already although I do wish the Rust manual would briefly mention this (and the equivalent feature in the dozen or so most popular languages, e.g. interfaces in Java). As it's currently written the Rust documentation makes traits seem like some novel and powerful thing which, as far as I can tell they really aren't. Contrast this with, say, the borrow checker which has nothing even remotely close in any production language/compiler besides Clean and Mercury. – user4718 Apr 29 '17 at 18:05
  • *Rust's type system can't compare the sizes of two types at compile time* Hrm, I thought being able to determine the size of every type (where the size of a Boxed type is the size of a pointer of course) was a major driver for many of the decisions in Rust's type system. Is there a description of the Rust type system for people with a PL background? Not necessarily expecting sequents and deductions, I realize Rust is still evolving quickly and the kind of formalism that ML has comes at the cost of slow evolution. – user4718 Apr 29 '17 at 18:09
  • @user4718 "*... can't compare the sizes ...*" -> my formulation was rather vague. What I mean by that is, that you (as a normal user) can't write a function with two type parameters and assert at compile time that the types' sizes are equal. You can of course check it at run time and `panic!()`, but that's not as good. This all has to do with the ability to play with integers on a type level which is currently not possible. Of course the compiler knows about sizes, the user just can't "use" them at compile time. Hope it's clearer now. – Lukas Kalbertodt Apr 29 '17 at 18:13
3

To map container to container, iterate and collect() them:

vec.iter().map(|x| x * 2).collect::<Vec<_>>();

To alter items in-place, use a for loop and iter_mut(). Using iterator chaining in this case is discouraged, since modifying values introduce side-effects.

for x in vec.iter_mut() { 
    x *= 2; 
}
Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
Tatsuyuki Ishi
  • 3,883
  • 3
  • 29
  • 41