Chapter I: Calculator

For the first program (except Hello world of course!) let’s create a simple CLI calculator. We start with a clean file in your favorite text editor:

// calc.v

Operations

We’ll start with the addition. In V a function is created by fn keyword and a basic numeric type “integer” is noted as int:

// calc.v
fn add(left int, right int) int {
    return left + right
}

If we run the file:

v run calc.v

nothing happens because we are missing the entrypoint for the executable, or simply said, a place where computer should begin the execution of a program. In V the entrypoint is known as a main function declared as fn main(). Let’s add a main() function that adds two integers and displays the output in the console. For that we’ll use a built-in println(s string) function:

// calc.v
fn add(left int, right int) int {
    return left + right
}

fn main() {
    println(add(1, 2))
}

Now we can see the output. Let’s include the rest of +-*/ operations in the same way we created add() function:

// calc.v
fn add(left int, right int) int {
    return left + right
}

fn sub(left int, right int) int {
    return left - right
}

fn mul(left int, right int) int {
    return left * right
}

fn div(left int, right int) int {
    return left / right
}

fn main() {
    println(add(1, 2))
    println(sub(1, 2))
    println(mul(1, 2))
    println(div(1, 2))
}

As we call each of the functions to perform a basic operation, we will see this output. Noticed something strange?

3
-1
2
0

1 / 2 is apparently 0. But why? Shouldn’t the output be 0.5?

Yes and no. For now we use int type which does not allow a decimal mark and values after that symbol. There is a different type that allows it written as f32 (short for 32-bit floating-point number).

Switch all types from int to f32 and run the program again.

// calc.v
fn add(left f32, right f32) f32 {
    return left + right
}

fn sub(left f32, right f32) f32 {
    return left - right
}

fn mul(left f32, right f32) f32 {
    return left * right
}

fn div(left f32, right f32) f32 {
    return left / right
}

fn main() {
    println(add(1, 2))
    println(sub(1, 2))
    println(mul(1, 2))
    println(div(1, 2))
}
3.000000
-1.000000
2.000000
0.500000

Nice, we can see a proper result for 1 / 2 operation.

Input

We can see the program computing results, but it’s only for the hard-coded values directly in the calc.v file. This way we’d always need to rewrite our calculator.

As you’ve already noticed when running a V program, V already knows what file you want to use by you providing a filename in the console. The same way V can use your value for compiling we can use it for computing results. The input values can be fetched with the help of os module.

In V you can use a module by importing it via import keyword. From that module we will need a constant os.args that returns an array of another kind of V type - string.

import os

fn add(left f32, right f32) f32 {
    return left + right
}

fn sub(left f32, right f32) f32 {
    return left - right
}

fn mul(left f32, right f32) f32 {
    return left * right
}

fn div(left f32, right f32) f32 {
    return left / right
}

fn main() {
    println(add(1, 2))
    println(sub(1, 2))
    println(mul(1, 2))
    println(div(1, 2))
    println(os.args)
}

Now you should see even the array of arguments passed to the calculator program the same as below:

3.000000
-1.000000
2.000000
0.500000
["./calculator-basic-ops-float"]

The first argument will always be a name of the executable that’s running, which in this case means ./calc, and anything other added after the path to the executable is added as the next argument.

# call
v run calc.v argument
./calc argument

# output
...
["./calc", "argument"]

Pseudo-stack with array

Once we can access the console arguments, we can quickly implement so called Reverse Polish notation with a for loop, if conditional and stack.

First we skip the executable path:

import os

fn main() {
    for idx, value in os.args {
        if idx == 0 {
            continue
        }
        println(value)
    }
}

To implement Reverse Polish notation we will use an array as a pseudo-stack structure for storing the f32. To create a variable V uses a simple <name> := <value> syntax e.g. number := 1, however for an array there is a catch. We need to go a little bit further and specify the type of all values in it as <name> := []<type>.

fn main() {
    stack := []f32
    stack << 1.2  // error
}

After we use a keyword mut, we mark the variable as editable and can use << operator for the array to append a new value to it.

Currently there is no quick way for popping the last element from an array while removing it at the same time, therefore we will access the last element by its position in angle brackets (array[idx]) in the array and then use array.delete() to remove it.

fn main() {
    mut stack := []f32

    // push
    stack << 1.2
    stack << 2.2
    stack << 3.2

    // print the array and its length
    println(stack)
    println(stack.len)

    // pop last item
    temp := stack[stack.len - 1]
    stack.delete(stack.len - 1)

    // print the array and the popped item
    println(stack)
    println(temp)
}

As you can see, an array has an array.len attribute. It’s changed on each resizing manipulation of array.

Back to the calculator code, we will use this pseudo-stack with manually pushing and removing items to implement Reverse Polish notation from console arguments.

We’ll create two array “buckets” for two categories of operators according to their precedence in an ordinary calculator.

After that let’s take care of an obvious error that might be raised - calling an operator function when there isn’t enough values on the stack. We need to check the number of elements in the stack array by its len attribute as we did for popping the values from it in previous example and then exit the program with a warning for which we’ll use panic(s string).

We can concat a variable, to a different string by using $ symbol and a name of a variable in a string like this: println("Value: $my_variable").

fn main() {
    // RPN stack
    stack := []f32
    prioritized := ["mul", "div"]
    normal := ["add", "sub"]

    for idx, value in os.args {
        if idx == 0 {
            continue
        }

        println(value)
        if (value in prioritized || value in normal) && stack.len < 2 {
            panic("No values to use for operator $value")
        }
    }
}

Now let’s append the console arguments on the stack if they are not one of the operator functions’ names we added to the arrays. By default any value from the console arguments is a string which means we are missing one step. We need to convert the value to f32 before appending it. By looking at the string implementation we can find its function for conversion string.f32(s string) which we should use before trying to append the value on the stack.

Once the values are converted and on the stack, we need to check if there are at least two and in the next console argument is an operator function’s name pop the values into left and right variables which will be used for printing out the result.

fn main() {
    // RPN stack
    mut stack := []f32
    prioritized := ["mul", "div"]
    normal := ["add", "sub"]

    for idx, value in os.args {
        if idx == 0 {
            continue
        }

        if (value in prioritized || value in normal) {
            if stack.len < 2 {
                panic("No values to use for operator $value")
            } else {
                right := stack[stack.len - 1]
                stack.delete(stack.len - 1)

                left := stack[stack.len - 1]
                stack.delete(stack.len - 1)

                if value == "add" {
                    println(add(left, right))
                } else if value == "sub" {
                    println(sub(left, right))
                } else if value == "mul" {
                    println(mul(left, right))
                } else if value == "div" {
                    println(div(left, right))
                } else {
                    println("$left $value $right")
                    println(stack)
                    panic("This should not happen!")
                }
                continue
            }
        }

        if (value in prioritized || value in normal) && stack.len < 2 {
            panic("No values to use for operator $value")
        }

        if !(value in prioritized || value in normal) {
            stack << value.f32()
        }
    }
}

Now we can check some basic instructions for our calculator this way:

// v calc.v
// ./calc 1.5 2 add
3.500000
// ./calc 1.5 2 sub
-0.500000
// ./calc 1.5 2 mul
3.000000
// ./calc 1.5 2 div
0.750000

Although usable, it’s very limited as it does not provide any option for joined operations such as (1 + 2) * (3 / (4 + 5)). First we need to convert this operation into Reverse Polish notation instructions so we can appropriately edit the main() function.

(1 + 2) * (3 / (4 + 5)) = 1
1 2 + 3 4 5 + / *
1 2 add 3 4 5 add div mul

// each value is added on the stack as present
// computing starts immediately after an operator is present and the two
// closest values on the stack are popped in the reversed order i.e.
// right first, left second
1
1 2
1 2 +
3
3 3
3 3 4
3 3 4 5
3 3 4 5 +
3 3 9
3 3 9 /
3 1/3
3 1/3 *
1
= 1

For that we need to rework the result handling a bit and put it on stack instead of printing out right away - switch println(operator(left, right)) to stack << operator(left, right) and then, after the computation is done, make sure there are no console arguments remaining. Then print the whole stack back to the console.

Note

Optimal result is having only a single element present on the stack, however it can happen that there will be an additional result if we provide more values than operators + 1.

Here is the complete solution. Obviously it can be optimized and refactored further, but that I’ve kept for you to have fun.

import os

fn add(left f32, right f32) f32 {
    return left + right
}

fn sub(left f32, right f32) f32 {
    return left - right
}

fn mul(left f32, right f32) f32 {
    return left * right
}

fn div(left f32, right f32) f32 {
    return left / right
}

fn main() {
    // RPN stack
    mut stack := []f32
    prioritized := ["mul", "div"]
    normal := ["add", "sub"]

    for idx, value in os.args {
        if idx == 0 {
            continue
        }

        if (value in prioritized || value in normal) {
            if stack.len < 2 {
                panic("No values to use for operator $value")
            } else {
                right := stack[stack.len - 1]
                stack.delete(stack.len - 1)

                left := stack[stack.len - 1]
                stack.delete(stack.len - 1)

                if value == "add" {
                    stack << add(left, right)
                } else if value == "sub" {
                    stack << sub(left, right)
                } else if value == "mul" {
                    stack << mul(left, right)
                } else if value == "div" {
                    stack << div(left, right)
                } else {
                    println("$left $value $right")
                    println(stack)
                    panic("This should not happen!")
                }

                if idx == os.args.len - 1 {
                    println(stack)
                    exit(0)
                } else {
                    continue
                }
            }
        }

        if (value in prioritized || value in normal) && stack.len < 2 {
            panic("No values to use for operator $value")
        }

        if !(value in prioritized || value in normal) {
            stack << value.f32()
        }
    }
}