Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to invoke a multi-argument function without creating a closure?

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.

like image 987
ObliqueMotion Avatar asked Feb 21 '26 15:02

ObliqueMotion


1 Answers

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.

like image 191
Shepmaster Avatar answered Feb 23 '26 06:02

Shepmaster