Chapter III: Word counter

Counting can be a very simple example which can help explaining the grouping of some variables under one roof. Let’s jump straight into fetching console arguments via os and define three modes this program will work with:

  • -w or --words

  • -l or --lines

  • -c or --chars

import os


fn parse_mode(args []string) string {
    mut mode := ""
    words := ["-w", "--words"]
    lines := ["-l", "--lines"]
    chars := ["-c", "--chars"]

    if args[1] in words {
        mode = "words"
    } else if args[1] in lines {
        mode = "lines"
    } else if args[1] in chars {
        mode = "chars"
    }
    return mode
}


fn main() {
    mode := parse_mode(os.args)
    println("Mode: $mode")
}

To generalize the mode we create a container for it - a struct - with struct keyword. The container will hold the mode’s name, its console arguments and later some other attributes. By default everything stored in a struct is immutable and pretty much inaccessible which allows us to have efficient abstractions without hacky hot-fixes unless we explicitly allow them.

struct Mode {
    name string
    cli_args []string
}

Once the struct is declared with name, curly brackets and its attributes it’s ready for usage. Although there are multiple ways for populating its attributes with values, we’ll use explicitly stated attribute names before values. This allows us to specify the attributes in an unordered way and it’s quite important in the long run from the project maintainability perspective due to no requirement of keeping the order of the struct attributes (which may change by including a new feature).

import os

struct Mode {
    name string
    cli_args []string
}


fn parse_mode(args []string) Mode {
    mut mode := Mode{}
    words := Mode{name: "words", cli_args: ["-w", "--words"]}
    lines := Mode{name: "lines", cli_args: ["-l", "--lines"]}
    chars := Mode{name: "chars", cli_args: ["-c", "--chars"]}

    if args[1] in words.cli_args {
        mode = words
    } else if args[1] in lines.cli_args {
        mode = lines
    } else if args[1] in chars.cli_args {
        mode = chars
    }
    return mode
}


fn main() {
    mode := parse_mode(os.args)
    println("Mode: $mode.name")
}

Now we can propagate each struct that’s initialized out of the function. Although V presents itself as not having a global space for symbols, const keyword creates a very similar space as global one, but on the module level. Since we can’t rewrite the value assigned as a constant, in combination with import keyword the module space is quite a powerful and safe feature.

import os

struct Mode {
    name string
    cli_args []string
}


const (
    words = Mode{name: "words", cli_args: ["-w", "--words"]}
    lines = Mode{cli_args: ["-l", "--lines"], name: "lines"}
    chars = Mode{name: "chars", cli_args: ["-c", "--chars"]}
)


fn parse_mode(args []string) Mode {
    mut mode := Mode{}

    if args[1] in words.cli_args {
        mode = words
    } else if args[1] in lines.cli_args {
        mode = lines
    } else if args[1] in chars.cli_args {
        mode = chars
    }
    return mode
}


fn main() {
    mode := parse_mode(os.args)
    println("Mode: $mode.name")
}

Each mode needs some kind of configuration so the counter knows what to use for distinguishing between words, lines or characters. This configuration we name sep as in separator and set it to <space> for words, \n for lines and <empty string> for characters. Note that the last one will count even <space> or \n as a character.

struct Mode {
    name string
    cli_args []string
    sep string
}


const (
    words = Mode{name: "words", cli_args: ["-w", "--words"], sep: " "}
    lines = Mode{cli_args: ["-l", "--lines"], name: "lines", sep: "\n"}
    chars = Mode{name: "chars", cli_args: ["-c", "--chars"], sep: ""}
)

To count we need to fetch the path from os.args, open the file and process its contents with a counting function that will use currently active counting mode. To open a file os.read_file(path string) is used which returns the file contents and also closes the file. Nevertheless, we still need to ensure such a file is present on the system with os.file_exists(_path string).

fn count(mode Mode, path string) int {
    mut result := 0

    if !os.file_exists(path) {
        result = -1
        return result
    }

    content := os.read_file(path) or {return result}
    for item in content {
        if "" in mode.sep || item.str() in mode.sep {
            result++
        }
    }
    return result
}

There is one catch with the os.read_file(path string) function, it returns an Option type. This kind of type has to be handled in your code with an or block that allows only specific set of keywords.

Once we handle the failing function and remove unnecessary printing to the console the program is ready and complete.

Here is a challenge for you as a reader: Currently it handles only a single file. Try to make it handle multiple files!

import os

struct Mode {
    name string
    cli_args []string
    sep []string
}


const (
    words = Mode{name: "words", cli_args: ["-w", "--words"], sep: [" ", "\n"]}
    lines = Mode{cli_args: ["-l", "--lines"], name: "lines", sep: ["\n"]}
    chars = Mode{name: "chars", cli_args: ["-c", "--chars"], sep: [""]}
)


fn parse_mode(args []string) Mode {
    mut mode := Mode{}

    if args[1] in words.cli_args {
        mode = words
    } else if args[1] in lines.cli_args {
        mode = lines
    } else if args[1] in chars.cli_args {
        mode = chars
    }
    return mode
}


fn count(mode Mode, path string) int {
    mut result := 0

    if !os.file_exists(path) {
        result = -1
        return result
    }

    content := os.read_file(path) or {return result}
    for item in content {
        if "" in mode.sep || item.str() in mode.sep {
            result++
        }
    }
    return result
}


fn main() {
    mode := parse_mode(os.args)
    file := os.args[2]
    println(count(mode, file))
}