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))
}