Coding Challenge, a Language Comparison

cvoell published on
9 min, 1629 words

Categories: programming

Tower of babel programming

Which language should I use on interviews?

So you're a "Senior Engineer" who has been developing for over 10 years and you've done significant projects in multiple programming languages AND you have an interview coming up where you'll have to do some coding challenges.

Should you use the language that you've used for the most years overall (muscle memory) or the newer language that you've used more recently?

A Leetcode comparison

One of the standard easy questions tackled first on Leetcode is called the two-sum problem. See the problem definition below:

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

Example 1:
Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:
Input: nums = [3,2,4], target = 6
Output: [1,2]

Code Comparison: Java, TypeScript, Rust, and Kotlin

Java

Java is my first instinct when I turn to coding challenges. I've used it so long, I actually have most common functions memorized. I also have used it in coding interviews in the past, as well as during my college data structure and algorithms courses (though that was a long time ago!).

Let's take a look:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> check = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (check.containsKey(complement)) {
                return new int[] {i, check.get(complement)};
            }
            check.put(nums[i], i);
        }
        return null;
    }
}

This code has a calming effect on me. There really aren't any wasted lines (answer is 13 lines overall). The initialization of the array is a little clumsy, but it works. The return null at the bottom is a reminder of the dangers of java, but for this quetion that guarantees at least one solution, return null is a quick an easy way to get our solution to compile. The run time is 201 ms. As of now, I don't have any strong opinions, but maybe that's a good thing!

TypeScript

TypeScript is a new kid on the block for sure. It blends the ubiquity of JavaScript with the type safety of Java and other more "serious" languages.

It exists to be a safer version of widespread of JavaScript, but as you can see in the code below it's not really getting in our way.

function twoSum(nums: number[], target: number): number[] {
    let hashMap = new Map<number, number>();
    for (let i = 0; i < nums.length; i++) {
        let complement = target - nums[i];
        if (hashMap.has(complement)) {
            return [i, hashMap.get(complement)];
        }
        hashMap.set(nums[i], i);
    }
    return null;
};

We've reduced the total lines from 13 to 11 thanks to leetcode not requiring a surrounding "Solution Object" in TypeScript, reflecting the functional programming mindset the JavaScript and TypeScript community seems to be trending towards. Otherwise the number of lines is exactly the same as Java. A few words have gotten more slim. We have Map instead of HashMap(I guess HashMap is the default type of Map in TypeScript which makes sense since it's the one people would use most often for constant lookup times), we have the slick has method instead of the clunky, yet precise containsKey, and we use set instead of Java's put. Also notice that the array initialization in the return statement has gotten a bit more streamlined.

It's clear that TypeScript is a slightly more slick language that would do fine in this type of simple coding question. The one fear would be that their slightly more opinionated style could result in not immediately knowing key function names.

Run Time 61 ms. Total lines 12.

Rust

Rust is a language that cares more about being efficient and safe than being easy, so how does it compare in this simple programming exercise?

We do come in one line over the Java implementation because we need to add an "import" of HashMap, via a use statement. This seems more of a leetcode configuration issue than something to subtract Rust for, but it is on brand for Rust being a lower level language that brings you closer to the metal, with less unneeded extra on top.

Taking a look at the code, there seems to be a lot "extra" in terms of cognitive load for solving this simple programming challenge.

use std::collections::HashMap;
impl Solution {
    pub fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {
        let mut hash_map: HashMap<i32, i32> = HashMap::new();
        for i in 0..nums.len() {
            let complement = target - nums[i];
            if let Some(&val) = hash_map.get(&complement) {
                return vec![i as i32, val];
            }
            hash_map.insert(nums[i], (i as i32));
        }
        vec![-1, -1]
    }
}

The first thing we notice, is we've traded our ArrayList and simple Arrays for a Vec, and the simple int is replaced with the more descriptive i32. So far, I'd say things are different but definitely not worse.

The next thing we'll notice is the mut keyword in front of the HashMap declaration. It's understandable that a mut keyword would be helpful for defaulting things to be not updateable, but then again, why would you ever initialize an empty HashMap that is not updateable?

Our for loop gets a more modern upgrade over the typical java syntax which is nice. Everything else is looking pretty standard, until we get to the interesting ampersand in front of complement. People who know rust know that the borrow sign & here is used for memory management, and that this memory de-allocation mechanism ends up speeding up rust relative to garbage collector languages like java. However, it is another instance of something that seems like a bit too much cognitive overload for a programming question.

The next noticeable line is the return statement which uses the vec! macro and has to specify that the iterator value be converted to i32 (as well as the second use of the beloved & borrow symbol). The vec! macro is a great convenience feature in Rust, but it does increase the cognitive load that you need to consider the macro feature for simple things like "list initialization".

Overall we have to admit that there is a higher cognitive load than what is necesary in the rust answer, but we also must acknowledge that this is the only answer with sub millisecond run time, and that none of the extra cognitive load seems wasteful from a language perspective, just extra in the context of a programming challenge.

Kotlin

So far we have Java as the most familiar, though showing signs of aging, TypeScript the most slick, but with signs of stylistically opinionated that could be hard to anticipate unless already familiar, and Rust as the extra cognitive load requiring speefy lowlevel control that is admirable yet likely not the best pic for programming languages.

Next up we have Kotlin, the more modern Java replacement. Could it possibly combine the best of all three? My first attempt made me think otherwise, because of the extra required Null protection, as seen below

class Solution {
    fun twoSum(nums: IntArray, target: Int): IntArray {
        var hashMap = HashMap<Int, Int> ()
        for (i in 0..nums.size) {
            val complement = target - nums[i]
            if (hashMap.containsKey(complement)) {
                val index = hashMap.get(complement);
                if (index != null) {
                    return intArrayOf(i, index)
                }
            }
            hashMap.put(nums[i], i)
        }
        return  intArrayOf(-1, -1)
    }
}

But technically we could just put it back to the unsafe java version as seen below. It's probably not fair to deduct points, since having the compiler force us to put !! by the potential NPE unless we deal with it can sometimes be a helpful reminder.

class Solution {
    fun twoSum(nums: IntArray, target: Int): IntArray {
        var hashMap = HashMap<Int, Int> ()
        for (i in 0..nums.size) {
            val complement = target - nums[i]
            if (hashMap.containsKey(complement)) {
                return intArrayOf(i, hashMap.get(complement)!!)
            }
            hashMap.put(nums[i], i)
        }
        return  intArrayOf(-1, -1)
    }
}

With the help of ChatGPT I was able to remove the !! and the extra lines of code using the kotlin specific ?.let statment.

class Solution {
    fun twoSum(nums: IntArray, target: Int): IntArray {
        val hashMap = HashMap<Int, Int>()
        for (i in 0 until nums.size) {
            val complement = target - nums[i]
            hashMap[complement]?.let { return intArrayOf(i, it) }
            hashMap[nums[i]] = i
        }
        return intArrayOf(-1, -1)
    }
}

With the code above, maybe we have acheived some ultimate version of safe, concise, readable code. However, the key space saving line is a bit confusing to me to be honest. I personally prefer the Some/None responses in Rust. But that probably comes down to personal preference and familiarity more than anything else.

Conclusions

As an ex Java Developer with short Kotlin experience before my current role splitting time between Rust and TypeScript, I do not have a super straightforward answer on which language I should use for coding questions. TypeScript seems the most widespread of the languages, yet it feels like it's better suited for front end and smaller web based projects than for large libraries that would benefit from the extra protections added in to Rust and Kotlin.

If I had to do a tricky problem with no help today, I'd use java because I know I have the correct function names and structures memorized.

As I practice more, I'll probably try and do most problems in at least two languages, using some combination of Java, TypeScript, and Rust.

Either way, this comparison, even on this simple example, was enlightening for me. If I find more interesting insights by comparing solutions across languages on more complicated programming challenges, I'll have to make a part two to this post.

Thanks for reading!