DEV Community

loading...
Cover image for Learning Programming Languages with Code Challenges

Learning Programming Languages with Code Challenges

jorge_rockr profile image Jorge Ramón ・9 min read

I don't know about you but sometimes I get bored by using the same tools over and over again.

So to have fun I wanted to learn a new programming language but as you may think I don't have the time to do so, maybe buying an Udemy course or watch some videos on YouTube are good idea but still... I wanted to do more than just writing Hello World.

But there was a bigger problem... which programming language should I be learning? I had the following options in mind:

  • Golang, is a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.
  • Rust, is a modern systems-level programming language designed with safety in mind. It provides zero-cost abstractions, generics, functional features, and plenty more.
  • Crystal, is a programming language with the following goals:

    • Have a syntax similar to Ruby (but compatibility with it is not a goal).
    • Be statically type-checked, but having to specify the type of variables or method arguments.
    • Be able to call C code by writing bindings to it in Crystal.
    • Have compile-time evaluation and generation of code, to avoid boilerplate code.
    • Compile to efficient native code.
  • Kotlin, is a general purpose, open source, statically typed “pragmatic” programming language for the JVM and Android that combines object-oriented and functional programming features.

  • Elixir, is a dynamic, functional language designed for building scalable and maintainable applications.

Hard decision, isn't it? but then I got an idea 💡... What about learning all those programming languages at once?

This is an experiment to see if it's possible to learn many programming languages at once by solving the same code challenge.

The rules

Before go on, we need some rules in order to do the experiment the best as possible:

  1. Pick a non-trivial challenge.
  2. Write a pseudocode solution following Cal Poly pseudocode standard.
  3. Write the code for each programming language.
  4. Write unit tests for each programming language.
  5. Explain what I learned.

If you know a better solution or code improvement of any programming language feel free to leave a comment and it will be mentioned in the next post. It's about continual improvement 🤓.

Table of Contents

  1. Code Challenge
  2. Pseudocode Solution
  3. Golang implementation
  4. Rust implementation
  5. Crystal implementation
  6. Kotlin implementation
  7. Elixir implementation
  8. Conclusion

Code Challenge: FizzBuzz

Let's start easy, FizzBuzz is a small game that interviewers use to determine whether the job candidate can actually write code. Here is the description:

Write a program that prints the numbers from 1 to 100. But for multiples of three print "Fizz" instead of the number and for the multiples of five print "Buzz". For numbers which are multiples of both three and five print "FizzBuzz".

Pseudocode Solution

FOR each number "i" from 1 to 100
  IF i is divisible by 3 AND is divisible by 5 THEN
    PRINT "FizzBuzz"
  ELSE IF i is divisible by 3 THEN
    PRINT "Fizz"
  ELSE IF i is divisible by 5 THEN
    PRINT "Buzz"
  ELSE
    PRINT i
  ENDIF
ENDFOR

Golang Implementation

Solution: fizzbuzz/fizzbuzz.go

package fizzbuzz

func FizzBuzz(number int) (string, bool) {
    if number%3 == 0 && number%5 == 0 {
        return "FizzBuzz", true
    } else if number%3 == 0 {
        return "Fizz", true
    } else if number%5 == 0 {
        return "Buzz", true
    } else {
        return "", false
    }
}

What I've learned

  • package keyword must be the first line in every Go program.
  • In this case the package name is fizzbuzz (same as directory name) because this is an utility package (i.e. it's not standalone executable).
  • To export the fizzbuzz function, its name must start with uppercase letter. Lowercased function names are private functions (i.e. can be only used in the same file).
  • A function must start with func keyword.
  • Like using Java or Javascript I wanted to return nil (null) if the given number is not divisible by 3 or 5 but that was a big mistake because the compiler throws the following error: cannot use nil as type string in return argument. So, since functions can return more than one value then fizzbuzz function returns two values; the first one (string) is the result text (Fizz, Buzz or FizzBuzz) and the second one (boolean) means if the operation was done successfully.
  • % (modulus) operator is used to check if the given number is divisible by 3 or 5.
  • if statements has no parenthesis.

Unit Tests: fizzbuzz/fizzbuzz_test.go

package fizzbuzz

import "testing"

func TestFizzCases(t *testing.T) {
    cases := [10]int{3, 6, 9, 12, 18, 21, 24, 27, 33, 36}

    for _, n := range cases {
        result, _ := FizzBuzz(n)
        if result != "Fizz" {
            t.Errorf("Error, %v should be 'Fizz'", n)
        }
    }
}

func TestBuzzCases(t *testing.T) {
    cases := [10]int{5, 10, 20, 25, 35, 40, 50, 55, 70, 80}

    for _, n := range cases {
        result, _ := FizzBuzz(n)
        if result != "Buzz" {
            t.Errorf("Error, %v should be 'Buzz'", n)
        }
    }
}

func TestFizzBuzzCases(t *testing.T) {
    cases := [10]int{15, 30, 45, 60, 75, 90, 105, 120, 135, 150}

    for _, n := range cases {
        result, _ := FizzBuzz(n)
        if result != "FizzBuzz" {
            t.Errorf("Error, %v should be 'FizzBuzz'", n)
        }
    }
}

What I've learned

  • testing is the built-int package to do tests (lol).
  • Every test function name must start with Test keyword and latter name should start with uppercase letter (for example, TestMultiply and not Testmultiply)
  • := is a short variable declaration (for example, i := 0 is the same as var i int = 0).
  • [N] T { v1, v2, ..., vN } is the way to declare an Array with fixed size N of type T with v1, v2, ..., vN as values.
  • Every test function has the same parameter.
  • There is no assert function or something like that, if the test fails it must use the t.Errorf function with a message.
  • range is the way to iterate over an Array or Slice (dynamic sized array), the first return value is the index and the second one is the value itself.
  • When a function returns more than one value, you can't ignore any of them (for example, result := FizzBuzz(n) is wrong because it returns two values).

Rust Implementation

Solution: fizzbuzz/src/lib.rs

pub fn fizzbuzz(number: i32) -> Option<&'static str> {
    if number%3 == 0 && number%5 == 0 {
        return Some("FizzBuzz");
    } else if number%3 == 0 {
        return Some("Fizz");
    } else if number%5 == 0 {
        return Some("Buzz");
    } else {
        return None;
    }
}

What I've learned

  • To create a new project just run cargo new [project-name] --lib(lib flag because it's not a standalone executable) and it will create the basic folder structure and the lib.rs file.
  • pub keyword is the way to export anything.
  • fn keyword is for functions.
  • There is no null value in Rust, so the best way to do the same is using the Option type. To give a value it must be Some(value), to give a "null" value it must be None.
  • if statements has no parenthesis
  • The type of a simple string needs the 'static lifetime (I don't know what does that mean... yet).

Unit Tests: fizzbuzz/src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fizz_cases() {
        let cases: [i32; 10] = [3, 6, 9, 12, 18, 21, 24, 27, 33, 36];

        for number in cases.iter() {
            let result: Option<&'static str> = fizzbuzz(*number);

            if let Some(text) = result {
                assert_eq!(text, "Fizz");
            } else {
                assert!(false);
            }
        }
    }

    #[test]
    fn buzz_cases() {
        let cases: [i32; 10] = [5, 10, 20, 25, 35, 40, 50, 55, 70, 80];

        for number in cases.iter() {
            let result: Option<&'static str> = fizzbuzz(*number);

            if let Some(text) = result {
                assert_eq!(text, "Buzz");
            } else {
                assert!(false);
            }
        }
    }

    #[test]
    fn fizzbuzz_cases() {
        let cases: [i32; 10] = [15, 30, 45, 60, 75, 90, 105, 120, 135, 150];

        for number in cases.iter() {
            let result: Option<&'static str> = fizzbuzz(*number);

            if let Some(text) = result {
                assert_eq!(text, "FizzBuzz");
            } else {
                assert!(false);
            }
        }
    }
}

What I've learned

  • #[whatever] is an attribute, useful to add metadata to a module, crate or item. In this case the module is using cfg attribute to add metadata to the compiler.
  • #[test] attribute is used to mark functions as unit tests.
  • mod whatever { ... } is the way to create a module. By default is private (unless pub keyword is added).
  • let keyword is used to create a variable (immutable by default).
  • [T;N] = [v1, v2, ..., vN] is the way to create an Array with fixed size N of type T with v1, v2, ..., vN as values.
  • for in iterates over an Iterator, so array.iter() returns an Iterator.
  • Maybe you are wondering why by calling fizzbuzz function I need to use *number and not number, well that's because array.iter() returns an Iterator with references (so, number variable type is &i32 and not i32).

Crystal Implementation

Solution: fizzbuzz/fizzbuzz.cr

module FizzBuzz
    def fizzbuzz(number)
        if number%3 == 0 && number%5 == 0
            return "FizzBuzz"
        elsif number%3 == 0
            return "Fizz"
        elsif number%5 == 0
            return "Buzz"
        else
            return nil
        end
    end
end

What I've learned

  • Syntax is very similar to Python and Ruby.
  • A module is a way to group functions and classes. It's more like a namespace.
  • def keyword is the way to create methods or functions.
  • nil is the null type.

Unit Tests: fizzbuzz/spec/fizzbuzz_spec.cr

require "spec"
require "../fizzbuzz"

include FizzBuzz

describe "FizzBuzz" do 
    it "should be 'Fizz' for multiples of 3" do
        [3, 6, 9, 12, 18, 21, 24, 27, 33, 36].each do |number|
            result = fizzbuzz(number)
            result.should eq("Fizz")
        end
    end

    it "should be 'Buzz' for multiples of 5" do
        [5, 10, 20, 25, 35, 40, 50, 55, 70, 80].each do |number|
            result = fizzbuzz(number)
            result.should eq("Buzz")
        end
    end

    it "should be 'FizzBuzz' for multiples of 3 and 5" do
        [15, 30, 45, 60, 75, 90, 105, 120, 135, 150].each do |number|
            result = fizzbuzz(number)
            result.should eq("FizzBuzz")
        end
    end
end

What I've learned

  • Import a package (spec in this case) or file (fizzbuzz solution) is very similar to Node.js.
  • include is very similar to Java's static import, I would have had to write every fizzbuzz call like Fizzbuzz::fizzbuzz without it.
  • Test look very similar to Mocha Framework in Javascript.

Kotlin Solution

Configuration: fizzbuzz/gradle.build

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.31'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation('org.jetbrains.kotlin:kotlin-stdlib:1.3.31')
    testImplementation('org.junit.jupiter:junit-jupiter-api:5.4.2')
    testRuntime('org.junit.jupiter:junit-jupiter-engine:5.4.2')
}

test {
    useJUnitPlatform()
}

What I've learned

  • Gradle configuration is just like any Java project, meh.
  • I tried to use built-in Test suite but I couldn't find out how, so I ended up using JUnit 5.

Solution: fizzbuzz/src/main/kotlin/FizzBuzz.kt

fun fizzbuzz(number: Int): String? {
    if (number%3 == 0 && number%5 == 0) {
        return "FizzBuzz"
    } else if (number%3 == 0) {
        return "Fizz"
    } else if (number%5 == 0) {
        return "Buzz"
    } else {
        return null
    }
}

What I've learned

  • I didn't need to create a class and I really liked that, just like any scripting programming language.
  • func keyword is the way to create a method/function.
  • Primitive types use uppercased letters.
  • String? return type is declared because it can be null.

Unit tests: fizzbuzz/src/test/kotlin/FizzBuzzTest.kt

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class FizzBuzzTest {

    @Test
    @DisplayName("It should be 'Fizz' for multiples of 3")
    fun shouldBeFizzForMultiplesOfThree() {
        val cases: IntArray = intArrayOf(3, 6, 9, 12, 18, 21, 24, 27, 33, 36)
        cases.iterator().forEach {
            assertEquals(fizzbuzz(it), "Fizz")
        }
    }

    @Test
    @DisplayName("It should be 'Buzz' for multiples of 5")
    fun shouldBeBuzzForMultiplesOfFive() {
        val cases: IntArray = intArrayOf(5, 10, 20, 25, 35, 40, 50, 55, 70, 80)
        cases.iterator().forEach {
            assertEquals(fizzbuzz(it), "Buzz")
        }
    }

    @Test
    @DisplayName("It should be 'FizzBuzz' for multiples of 3 and 5")
    fun shouldBeFizzBuzzForMultiplesOfThreeAndFive() {
        val cases: IntArray = intArrayOf(15, 30, 45, 60, 75, 90, 105, 120, 135, 150)
        cases.iterator().forEach {
            assertEquals(fizzbuzz(it), "FizzBuzz")
        }
    }
}

What I've learned

  • intArrayOf is the way to create an Array of Integers and the type is IntArray.
  • val is for readonly variables.
  • There is no way to iterate over the array itself, it's only through an Iterator.
  • forEach method receives a function and if there isn't an explicit parameter in the function definition then it will be named it by default.

Elixir Solution

Solution: fizzbuzz/lib/fizzbuzz.ex

defmodule Fizzbuzz do

  def fizzbuzz(number) do
    cond do
      rem(number, 3) == 0 && rem(number, 5) == 0 -> "FizzBuzz"
      rem(number, 3) == 0 -> "Fizz"
      rem(number, 5) == 0 -> "Buzz"
      true -> nil
    end
  end
end

What I've learned

  • defmodule is the way to create a module (a group of functions, think about a class in OOP).
  • Functions and variables are created with def keyword.
  • cond is like a switch statement but with boolean conditions.
  • Elixir is a functional programming language, so everything is a function.
  • nil is the null type.
  • rem is the way to calculate modulus.
defmodule FizzbuzzTest do
  use ExUnit.Case
  doctest Fizzbuzz

  test "should be 'Fizz' for multiples of 3" do
    Enum.each([3, 6, 9, 12, 18, 21, 24, 27, 33, 36], fn x -> 
      assert(Fizzbuzz.fizzbuzz(x) == "Fizz") 
    end)
  end

  test "should be 'Buzz' for multiples of 5" do
    Enum.each([5, 10, 20, 25, 35, 40, 50, 55, 70, 80], fn x -> 
      assert(Fizzbuzz.fizzbuzz(x) == "Buzz") 
    end)
  end

  test "should be 'FizzBuzz' for multiples of 3 and 5" do
    Enum.each([15, 30, 45, 60, 75, 90, 105, 120, 135, 150], fn x -> 
      assert(Fizzbuzz.fizzbuzz(x) == "FizzBuzz") 
    end)
  end
end

What I've learned

  • fn is an anonymous function (like a lambda).
  • Enum module is useful to work with Collections.

Conclusion

Golang, Rust were the hardest ones, but I can't wait to learn more about them.
Kotlin is much like Java and was the only programming language that needed an external build system to run the code.

Thank you so much for reading, Did you like the experiment? 🤓.

Here is the Github repository:

GitHub logo jorgeramon / learning-languages

Repository of "Learning Programming Languages with Code Challenges" post series.

Learning Programming Languages with Code Challenges

Programming Languages

  1. Golang
  2. Rust
  3. Crystal
  4. Kotlin
  5. Elixir

Challenges

  1. Fizz Buzz

Write a program that prints the numbers from 1 to 100. But for multiples of three print "Fizz" instead of the number and for the multiples of five print "Buzz". For numbers which are multiples of both three and five print "FizzBuzz".

Discussion

pic
Editor guide