3

I came across this while doing the 2018 Advent of Code (Day 2, Part 1) solution in Rust.

The problem to solve:

Take the count of strings that have exactly two of the same letter, multiplied by the count of strings that have exactly three of the same letter.

INPUT

abcdega 
hihklmh 
abqasbb
aaaabcd
  • The first string abcdega has a repeated twice.
  • The second string hihklmh has h repeated three times.
  • The third string abqasbb has a repeated twice, and b repeated three times, so it counts for both.
  • The fourth string aaaabcd contains a letter repeated 4 times (not 2, or 3) so it does not count.

So the result should be:

2 strings that contained a double letter (first and third) multiplied by 2 strings that contained a triple letter (second and third) = 4

The Question:

const PUZZLE_INPUT: &str = 
"
abcdega
hihklmh
abqasbb
aaaabcd
";

fn letter_counts(id: &str) -> [u8;26] {
    id.chars().map(|c| c as u8).fold([0;26], |mut counts, c| { 
        counts[usize::from(c - b'a')] += 1;
        counts 
    })
}

fn has_repeated_letter(n: u8, letter_counts: &[u8;26]) -> bool {
    letter_counts.iter().any(|&count| count == n)
}

fn main() {
    let ids_iter = PUZZLE_INPUT.lines().map(letter_counts);
    let num_ids_with_double = ids_iter.clone().filter(|id| has_repeated_letter(2, id)).count();
    let num_ids_with_triple = ids_iter.filter(|id| has_repeated_letter(3, id)).count();
    println!("{}", num_ids_with_double * num_ids_with_triple);
}

Rust Playground

Consider line 21. The function letter_counts takes only one argument, so I can use the syntax: .map(letter_counts) on elements that match the type of the expected argument. This is really nice to me! I love that I don't have to create a closure: .map(|id| letter_counts(id)). I find both to be readable, but the former version without the closure is much cleaner to me.

Now consider lines 22 and 23. Here, I have to use the syntax: .filter(|id| has_repeated_letter(3, id)) because the has_repeated_letter function takes two arguments. I would really like to do .filter(has_repeated_letter(3)) instead.

Sure, I could make the function take a tuple instead, map to a tuple and consume only a single argument... but that seems like a terrible solution. I'd rather just create the closure.

Leaving out the only argument is something that Rust lets you do. Why would it be any harder for the compiler to let you leave out the last argument, provided that it has all of the other n-1 arguments for a function that takes n arguments.

I feel like this would make the syntax a lot cleaner, and it would fit in a lot better with the idiomatic functional style that Rust prefers.

I am certainly no expert in compilers, but implementing this behavior seems like it would be straightforward. If my thinking is incorrect, I would love to know more about why that is so.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
ObliqueMotion
  • 483
  • 1
  • 5
  • 11
  • What you want to do is "currying" or "partial function application", that is not directly supported in Rust. Writing a lambda is actually the idiomatic way to do that. – rodrigo Dec 16 '18 at 21:59
  • What you suggest is actually quite dangerous, as you could forget to write any number of arguments in a function call and it will compile anyway. You'll need to add warnings about: "warning: partial function call, this code does nothing!". And diagnostics would get a lot messier. – rodrigo Dec 16 '18 at 22:01
  • Interesting. @rodrigo I don't understand why "you could forget to write any number of arguments in a function call and it will compile anyway." Why can it not be restricted to ONLY the single, last argument in a parameter list? Why would there be no way to check "compiler error: you are missing more than one argument in this function call." Handling the single case where only the very last argument can be left out does not seem dangerous to me, and it would automatically cover the case of a single-argument function, which is already supported. – ObliqueMotion Dec 16 '18 at 22:34
  • 3
    Imagine a function `fn foo(a: i32, b: i32) {...}`, then you call it `fn main() { foo(4); }`, with your proposal, this code would be legal but do nothing. Writing a lambda such as `|x| foo(4, x)` when it is really needed is far more reasonable, IMHO. Also you can as easily write `|x| foo(x, 4)` or any other variation with any number of arguments without compromising the rest of the language. – rodrigo Dec 16 '18 at 22:38
  • Okay, I see your point. Allowing that syntax would create a lot of extra work. – ObliqueMotion Dec 16 '18 at 22:57
  • In the interest of exploring possibilities, do you think one could make a macro that would work like this: `.filter(apply!(has_repeated_letter, 3))` which would take a function to apply to a list of arguments, the last of which being in the iterator. I'm not saying this would necessarily be good practice. It would obviously be cleaner at this point to make a closure. I just haven't played with macros much and I'm wondering if it is possible. – ObliqueMotion Dec 16 '18 at 23:01
  • 2
    Sure you can write a macro that takes those arguments and expands to a lambda, but then, what is the advantage of `apply!(has_repeated_letter, 3)` over `|x| has_repeated_letter(3, x)`? – rodrigo Dec 16 '18 at 23:05
  • Certainly no advantage at all! I think that answers all of my curiosities. Thank you! – ObliqueMotion Dec 16 '18 at 23:18
  • 2
    Could you not change the has_repeated_letter to return a closure like [this](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=af818d3815293947feec82e35ed2a17b) – Lukazoid Dec 16 '18 at 23:52
  • Lukazoid, that is a brilliant idea! Thank you for suggesting that. That really opens my mind to new possibilities. I'm not sure if returning a closure like that is necessarily more readable or more approachable than just creating the closure inline, but it does achieve the syntax I was looking for. I really appreciate the comment. – ObliqueMotion Dec 17 '18 at 00:24
  • 1
    For your next question, please consider reducing the amount of fluff. Your question basically boils down to "I can do this [one argument example], how can I do this [two argument example]" The amount of background (the Advent of Code is basically completely irrelevant to the question) will make this question much harder for *other* people to read it to know if it's relevant to them when they have a potential duplicate. – Shepmaster Dec 17 '18 at 01:32
  • Shepmaster, you are welcome to trim down the question to a form that you feel is more succinct. I agree with you. I may have time to do it later. You may do a better job than I will. Thanks for your answer. – ObliqueMotion Dec 17 '18 at 02:25

1 Answers1

4

No, you cannot pass a function with multiple arguments as an implicit closure.

In certain cases, you can choose to use currying to reduce the arity of a function. For example, here we reduce the add function from 2 arguments to one:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn curry<A1, A2, R>(f: impl FnOnce(A1, A2) -> R, a1: A1) -> impl FnOnce(A2) -> R {
    move |a2| f(a1, a2)
}

fn main() {
    let a = Some(1);
    a.map(curry(add, 2));
}

However, I agree with the comments that this isn't a benefit:

  1. It's not any less typing:

    a.map(curry(add, 2));
    a.map(|v| add(v, 2));
    
  2. The curry function is extremely limited: it chooses to use FnOnce, but Fn and FnMut also have use cases. It only applies to a function with two arguments.

However, I have used this higher-order function trick in other projects, where the amount of code that is added is much greater.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366