Comparing Code: LeetCode Problems in Rust vs. Haskell

Today will be the first in a series where we’ll be exploring some LeetCode problems and comparing different solutions from Haskell and Rust. The main idea is to demonstrate how you might translate ideas between the recursive core of Haskell, and the loop-based framing of most other languages.

If you want to learn more about problem solving in Haskell, you should take a closer look at Solve.hs! This course will give you an in-depth walkthrough of problem solving ideas in Haskell, including how concepts compare to more typical languages.

The Problem

The first problem we’ll consider is called H-Index. In academia, a person has an “H-Index” of n if they have published at least n papers that have n or more citations. So the input to our problem is a list of integers, where each integer is the number of citations of a particular paper the author wrote. Our job is to calculate the author’s H-Index.

The Algorithm

This problem is fairly straightforward if you sort the input list. Once we do this, we can look at any index i, and consider the number of remaining entries (e.g. n - i), and we’ll know that the number of papers with at least that many citations is n - i.

So we can accomplish this task with a single loop over the sorted list. Throughout this loop, we’ll be tracking the maximum “H-Index” we’ve seen so far (maxH). At each iteration, we take the following steps:

Get the number of remaining papers (rem) and the citations at this index (next) If the rem is greater than next, then update maxH to next if next is larger. Otherwise, update maxH to rem if rem is greater.

The last step is a key edge case! If we have the list [1, 1, 1, 9, 9], we’ll get to index 3, with next being 9 and rem being 2. The remainder is smaller than the index, but we would still update maxH to 2, because there are at least 2 citations remaining that are 2 or greater.

Rust Solution

Here’s our Rust solution:

pub fn h_index(citations: Vec<i32>) -> i32 {
    let mut cp = citations.clone();
    cp.sort();
    let n = cp.len();
    let mut maxH: i32 = 0;
    for i in 0..n {
        let next = cp[i];
        let rem: i32 = (n - i) as i32;
        if (rem >= next) {
            maxH = std::cmp::max(next, maxH);
        } else {
            maxH = std::cmp::max(rem, maxH);
        }
    }
    return maxH;
}

We have the first part, where we clone the input, sort it, and set up our loop variables:

pub fn h_index(citations: Vec<i32>) -> i32 {
    let mut cp = citations.clone();
    cp.sort();
    let n = cp.len();
    let mut maxH: i32 = 0;
    ...
}

Then we have the loop itself, where we have our two cases to consider:

for i in 0..n {
    let next = cp[i];
    let rem: i32 = (n - i) as i32;
    if (rem >= next) {
        // There are at least ‘next’ papers >= ‘next’
        maxH = std::cmp::max(next, maxH);
    } else {
        // ‘next’ > ‘rem’, so there are at least ‘rem’ papers >= ‘rem’
        maxH = std::cmp::max(rem, maxH);
    }
}

So this is pretty straightforward. Now how do we approach this kind of problem in Haskell?

Haskell Solution

Our Haskell solution will have the same structure, but instead of running a loop and indexing into a vector, we’ll use a linked list and call a recursive function. Let’s begin by getting the length and sorting our input:

import qualified Data.List as L

hIndex :: [Int] -> Int
hIndex inputs = ...
  where
    n = length inputs
    sorted = L.sort inputs

    ...

Now we need to think about our recursive loop function. At each iteration, we need access to the remaining number of values, the next citation value, and we need to pass along maxH. As with many list-based recursive functions, we’ll peel off one element of the input list each time. Ultimately we’ll return maxH from this loop when we hit our base case of an empty input list. So its type signature should look like this:

loop :: (Int, [Int], Int) -> Int

When writing a recursive function, we always handle the base case first:

loop :: (Int, [Int], Int) -> Int
loop (_, [], maxH) = maxH

Now in the recursive case, we can apply our algorithm, updating maxH if necessary:

loop :: (Int, [Int], Int) -> Int
loop (_, [], maxH) = maxH
loop (remaining, next : rest, maxH) = if remaining >= next
  then loop (remaining - 1, rest, max next maxH)
  else loop (remaining - 1, rest, max remaining maxH)

To finish up, all we need to do is call our loop function with the appropriate initial inputs (n, sorted, 0). Here’s our complete Haskell solution:

import qualified Data.List as L

hIndex :: [Int] -> Int
hIndex inputs = loop (n, sorted, 0)
  where
    n = length inputs
    sorted = L.sort inputs

    loop :: (Int, [Int], Int) -> Int
    loop (_, [], maxH) = maxH
    loop (remaining, next : rest, maxH) = if remaining >= next
      then loop (remaining - 1, rest, max next maxH)
      else loop (remaining - 1, rest, max remaining maxH)

Using a Fold

Now we can notice that our loop has a particular structure. We have one piece of accumulated state (maxH), and this changes based on each value in our list (combined with the remaining values). We can easily re-imagine this kind of loop using a fold. We just have to think of the folding function like this:

loop :: Int -> (Int, Int) -> Int
loop maxH (remaining, next) = if remaining >= next
  then max next maxH
  else max remaining maxH

This has the a -> b -> a structure of a left-fold function, where a is our accumulated maxH value, and the other values come from our list. The main benefit here is that our loop function no longer has to deal with the burden of calling a base case or passing the “shrinking” list as an argument to the next recursive call.

We can invoke this loop at the top level like so:

hIndex :: [Int] -> Int
hIndex inputs = foldl loop 0 (zip [n,n-1..1] sorted)
  where
    n = length inputs
    sorted = L.sort inputs

    loop :: Int -> (Int, Int) -> Int
    loop maxH (remaining, next) = if remaining >= next
      then max next maxH
      else max remaining maxH

We just have to zip the decreasing indices together with our sorted list. Now our recursive “loop” is more like a typical for-loop. We’re only considering one element at a time, and we’re updating the important state each time.

Conclusion

In this comparison, we saw a good comparison between a normal for-loop in Rust, and a recursive solution in Haskell. We also saw how we could simplify this recursive formulation into a “fold” structure.

If you're interested in learning more about writing recursive functions in Haskell, check out our Solve.hs course. You’ll learn how to start thinking about problems in a functional way, and you’ll learn the step-by-step processes for tackling problems with basic recursion and folds like we saw in this example.

Next
Next

Hey ChatGPT, Write me a Haskell Course?