-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Welcome to the vlang-lessons-learnt wiki!
This repo is really only about documenting my experience with V-lang, gotchas, things to remember etc.. The information are in no particular order. I plan to gain a bit of experience with V first, before raising issues with the core developers. Mostly to avoid asking for stupid things.
-
"\0"
prints a warning that 0 chars are not allowed in strings (for easy C interoperability reasons I assume) - however
"\000"
(otcal representation) works fine -
"\x00"
, the"\x<hex>"
notation is not supported -
"\u<uuuu>"
for unicodes is also not supported
I think V is a bit confused about C-style \0
terminated strings and V-strings (len attribute). I fully understand that V's core needs to interact a lot with C-libs and C-interoperability should be easy for core and lib-developers. But should users care? Either V-string are C-style and \0
terminates a string, then we need a []byte that does not. Or the len attribute is used and \0
has no special meaning. By means special functions, e.g. from_cstring(), to_cstring() it might be handled. A CString struct probably required a lot of copy & paste, since V has no String-interface which would allow multiple different implementations.
if _ := fn_that_may_throw_an_error { assert false }
- Within the if / else block, the error message is available
This can also be used implement something like: If the preferred approach succeed, good. If it fails with an error, then use an alternative approach.
res := ""
if res = my_fn_that_raises_an_error() {
// ok path
} else {
res = another_function()?
}
- 'return' usually means "return from a function", but in an 'if' and 'match'-expression it is not allowed
fn my_test() ? {
a := if x == 1 {
"test"
} else {
return error("..")
}
...
}
This does not work as expected: the function will not return with an error. Instead the V-compiler will complain.
Replacing "test"
with a_function()?
will be accepted by the V-compiler, but the generated C-code will not compile.
Instead you need to do something like:
fn my_test() ? {
mut a := ""
if x == 1 {
a = "test"
} else {
return error("..")
}
...
}
V supports modules and for an application it seems to recommended to put them into a ./modules
subdirectory of the application. For a re-useable module, that you may want to use in multiple application, may be not the best place to put it.
V also has vpm, a package manager, and you can register / upload your module their, which, however makes it public, which you may want or may be not, if it is meant to be private. When downloading a module via vpm, it gets cached in ~/.vmodules. Vpm seems to have no option to register a module only locally, without uploading.
The v.mod
file has a dependencies
field, which can be filled with the names of the dependent modules. Directories however are not supported.
This offers the following option
- Use git sub-projects for every module to allow that each module has it's own git-repo. Not my favorite
-
..\v\v.exe -path "@vlib;../vlang-mmap/modules"
to update the search path for modules - Create a soft-link in ~/.modules to point to the directory where your module resides.
- Create a soft-link in your ./modules directory to point to the directory where your module resides.
Either of the last two options, is what I'm currently using.
v.mod: I haven't tested what will happen if you create a soft-link in .vmodules and then do an vpm upgrade
.
Win10 now supports soft-links as well: e.g. mklink /d $HOMEPATH/.vmodules/mmap $MY_VLIBS\modules\mmap
Also looking at the modules already registered in VPM you see that all modules have their source code in the main directory. After some weeks of developing the mmap and yaml modules, I don't like that very much. I prefer to have source code in some 'src' directory. In the root folder they are mixed for all sort of other files (v.mod, readme.md, .gitignore, and so on).
I'm also yet undecided whether I like source code and test files (and test data) in the same directory. My tendency is more to have it separate. I haven't checked, but what happens to test data when building the .exe file? E.g. in case of the YAML module, I have plenty test data files located in a subdirectory? Unfortunately the docs is not really clear on this.
What I also don't like is that you can have only one executable per directory. You need a 'main' module, and all *.v files in the same directory are considered part of the module. You must use a directory per executable strategy. Which also generates the executable in the that directory. I would prefer to generate them in the ./bin folder. The only way to achieve this right now: create your own little post-compiler script that copy the files. I think I mentioning it elsewhere already, but V has not real build-system with pre- and post-processors etc.. It basically is just the compiler.
I like the python approach to use ar[..10]
or ar[-10 ..]
, and when using a range to automatically make sure that
lower and upper boundaries are properly adjusted if needed. Unfortunately V-lang doesn't have it and you need to do
ar[.. math.max(0, ar.len - 10)]
and the like.
In that context, math.max() and friends seem to have issues with casting the return type. It seems they always
return f64 so that you actually need to write: ar[.. u64(math.max(0, ar.len - 10))]
to work.
V-lang has an assert
statement, but unfortunately it does not support an optional error message, such as
assert my_fn() == 0, "Expected xyz to provide whatever: $1 != $2"
assert
also does not support multiple return values, e.g. `assert 1, 2 == 1, 2
assert
is probably not ready yet, e.g. src := "abced"; assert byte(30) == src[0]
does not work. Assert will report
the right value as "unknown value". Whereas if you do src := "abced"; x := src[0]; assert byte(30) == x
it will do.
I had a function that was causing a divide-by-zero runtime error. I first didn't know because even though
the result said that a test failed, no output was printed as it usually happens when a test fails. I was confused
until I found the -stats
option (e.g. v -stats test .
) which prints a lot more info. And in my case, it also
printed the stack trace with the divide-by-zero error. => remember the -stats
option when v test .
fails but
does not print any output.
While on tests. I find it unfortunate that v test
has not cli option to stop test execution after the first failure.
Sometimes you make a breaking change and you need to review and fix all the test failures. Which you usually do one
after the other.
V internal structs such as string, bytes etc. are lowercase snake_case. Whereas, the examples in the documentation for user created structs is CamelCase. That is not consistent.
I like constants to be all UPPERCASE. Not recommended in V.
With import xyz
you can import a module. But you must fully qualify the function in the source code like xyz.my_fn()
,
which I like. And I fully agree that it makes it easier to read the code. Unfortunately V also has import xyz { my_fn }
which imports my_fn() into the namespace of my current module. It does not require a fully qualified name upon its use.
We have a little memory mapping modul, because we need to read files which are >10 GB. This is not possible with the built-in
string or []byte types, as there len variable is an int
which in V is 32bit. Which means strings can not have more then 2GB
chars. We developed our own large_string module with an i64 len field.
Unfortunately V has no convention which allows to use [ .. ]
with their structs. It is currently hard-coded in V. A convention,
such as ar[a .. b]
which get translated in ar.slice(a, b)
would be a simple approach to achieve this.
V comes with a source code formatter, which is a good idea. I'm all fine with it putting brackets where they belong etc., but I do not like that it moves and even destroys some of my comments. E.g.
struct X {
x int /* = 0 */ // This field ...
}
In that context, I also don't like that I'm not allowed to put in default default value. V prints a warning, which, when generating
production code with -prod
will fail with an error message. IMHO it make the code more readable to add default, even if its
system defaults.
Unfortunatly V doesn't seem to have conventions for implementing certain syntactic sugar, e.g.
ar[a .. b] to ar.range(a, b)
, or ar[i] to ar.at(i)
or ar << x to
ar.add(x)`.
V seems to have a next() convention for iterators, but it's kind of half only. E.g. struct string
has no field for
tracking the position in the iterator, but string does work in for s in "abc" {}
. Somehow magically some iterator
struct for string must be instantiated. In user created structs there is no such magic. You need to create a struct
that has a next() method. E.g. for x in MyIterator{ data: my_data }
. A convention could be that an iter()
function
gets invoked. so that for line in file
gets translated into:
mut iter := file.iter()
for {
if line := iter.next() {
...
} else {
break // End of iterator
}
}
Similarly for for i, line in file
Or like below, which I think makes it even easier create iterators. But it requires
an iterator
attribute and yield
keyword. Again the compiler would simply replace
for line file.line_iter()
pretty much with the iterator function body. An advantage,
compared to python, would be that exceptions are not silently swollowed, and the overhead
is virtually zero.
[iterator]
pub fn (file FWFile) line_iter() ?string {
for i in 0 .. file.records {
line := file.line(i)?
yield line
}
}
div-by-zero, invalid-array-index, etc. cause a panic and do not return an error. Unfortunately it is not possible to test that, as panics can not be caught, not even in test code. I understand some sort of recover is planned, but currently not available.
if obj is []int { eprintln(obj.len) }
is not working. It'll complain that obj
has no len
function.
I encountered this issue when trying to define some like:
type YamlValue = map[string]YamlValue | []YamlValue | string
I was positively surprised that V allows to use the type (recursively) within its own definition. But because of the smart-cast issue I had to create structs for the map and the list like
struct YamlListValue { ar []YamlValue }
struct YamlMapVakue { obj map[string]YamlValue }
type YamlValue = YamlMapValue | YamlListValue | string
Let's assume struct MyStruct {..}
then, in the absence of constructors, you may be tempted to use
fn new_MyStruct() MyStruct {..}
as a convention. Unfortunately that doesn't work with V. V is strict with upper-case
in struct names and lower-case in function names :(
A typical struct looks like
struct MyData {
this_is_private int
pub:
this_is_public_immutable int
pub mut:
this_is_public_mutable int
mut:
this_is_private_mutable int
}
Observations are:
- it is not possible to use any of ´put` access mutators multiple times. This forces the dev to put the variable into groups.
- There is no
private
. It always must come first
I'm undecided whether I like this or not. Usually I group my vars by purpose, which makes the source code more readable. But may be that is an old C++/Java experience, where structs or classes tend to be large. In V, the struct definition is usually not that long.
I'm more concerned with data placement, when the exact offset, length and padding is relevant for reading and writing binary data structures. Placement is not supported right now anyways, but don't see how it could.
Struct field which are private mutable and public immutable (read-only)? imho, enough adding [pub-read-only] attribute...
To safe me writing, I occassionaly want a reference to an array stored in a struct. The trap is that 'mut ar := mystruct.myarray' creates a copy. I wish V would raise a warning, as this is usually not what you want. V's map seem to have move()
and copy()
functions, which I think is a good idea as it makes the intend obvious.
Raising that question on the help channel, the answer was "don't do it" (create a reference). Well, I thought, they certainly have more experience then me, and I created a function that receives a mutable array like fn my_fn(mut ar []int)
. Fine, problem solved, I thought. Strangely the function did not compile. In short
struct MyData1 { pub mut: ar []int }
fn pass_array_mut(mut ar[]int) int {
if ar > 0 && ar.last() == 99 { return 99 }
return 0
}
fn test_pass_array() {
mut m := MyData1{}
m.ar << 99
assert pass_array_mut(mut m.ar) == 99
}
Looking at the C code then a ptr to array is provided, but the code produced for ar.last() assumes it is an array (not a ptr). It looks like the C-code generated for ar.last() (or any array function?) is wrong.
Imagine some code like: if rtn is YamlListValue { rtn = rtn.ar[p] }
and a compiler error message like "The error message may be field 'ar' does not exist or have the same type in all sumtype variants". The error message is correct, not all sumtype variants have an 'ar' field. But that is why I'm using smart cast in the first place. The solution is simple, but the error message is giving no hint in that direction: if mut rtn is YamlListValue { rtn = rtn.ar[p] }
. Just add mut
to the if-statement. My suggestion to the V-team: improve the error message.
I like Julia's and NIM's metaprogramming capabilities and ways of doing it, which is safe, compared to C macros. It allows to generate V-code at compile time. This is useful e.g. for
- regular expression compilation at compile time. Many REs are not that complicated and very efficient code could be generated
- Rosie is more advanced pattern matching library. The rosie files must be compiled, like source code. V's build process is not extendable. You can not trigger a pre-build step, like with a build tool.
- File reader (e.g. JSON, YAML, etc.) would benefit from auto-generating V-code matching the file structure
- Similarly ORM components. V's build-in ORM is nice for scripts and very simple apps. I wish it would be possible to generate V-code that matches the underlying database structure. Whether it reads the DB's metadata or uses some other sort of config is not important.
V uses attributes to support mapping of json data, but as of today, it is built-in and not extensible in any form.
I like how some other languages have f".."
or r".."
or .. and that it simply is syntactic should for some function, e.g. f_str(..)
, r_str(..)
Occassionaly I read about []! and []!!, which is not documented anywhere. I think it has something todo with where the array gets allocated: static, stack, heap, but that is only a guess
Consider:
type YamlTokenValueType = string | i64 | f64 | bool
fn (obj YamlTokenValueType) str() string { return "xxx" }
A bit surprisingly for me, this seems to be working. Which is a pleasant surprise. But I don't think it is documented, hence I'm not sure it is planned or by accident.
See (here)[https://github.com/vlang/v/issues/10898) for additional details.
.. while you defined str() methods on the sumtype variants, you never defined a custom str() method on the sumtype itself. By default sumtypes (and interfaces, type aliases, etc.) are always printed as a cast, so the behavior you're seeing is working as intended.
If you do not want the sumtype to be printed as a cast for whatever reason, you need to define a custom str() method on the sumtype itself.
IMHO Vlang is not consistent here. type_name() only consists for sumtype (may be also interfaces, I don't know). But simple struct don't. For structs you shall use typeof(obj).name, and obj.type_name() raises a compiler error.
Also see (here)[https://github.com/vlang/v/blob/master/doc/docs.md#profiling]
This is copy and paste from the help channel, so that I don't forget about it.
You can do: ./v -profile x.txt run examples/path_tracing.v
. Please note that -profile does currently not work with *_test.v files!! After your program finishes, open the x.txt
in a text editor, it will have something like this in it:
64403 15.769ms 245ns strings__new_builder
2 0.001ms 452ns strings__Builder_write_ptr
22 0.008ms 359ns strings__Builder_write_b
385209 152.446ms 396ns strings__Builder_write_string
64403 34.304ms 533ns strings__Builder_str
64403 8.111ms 126ns strings__Builder_free
The first column is the number of invocations of each function, the second is the cumulative time spent in each function, the 3rd is the average time (2nd / 1st), and the 4th is the C name of the function. Another way (which is linux specific afaik) is to use valgrind
- compile your program: ./v -cc gcc-10 -g -keepc examples/path_tracing.v
- run it under valgrind: valgrind --tool=callgrind ./examples/path_tracing
- that will produce a callgrind.out.PID file
- run kcachegrind callgrind.out.PID
kcachegrind
offers a very nice interactive way to visualise the data - you can sort by various metrics, focus on specific functions etc
so if you use linux, I highly recommend it.
Heaptrack
is also a nice tool, if you are looking to optimise memory usage
- compile your program (same as before)
- heaptrack ./examples/path_tracing
- that will produce a heaptrack.path_tracing.PID.zst file
- visualise it with heaptrack --analyze heaptrack.path_tracing.PID.zst
afaik, it is also a linux only tool
As of writing this entry, I'm using 'V 0.2.2 5452ba4', which is the latest. So far I'm mostly ok with V, but I would rate the compiler maturity currenty 'alpha'. Be prepared for unexpected compiler crashes, generated C-code that doesn't compile, workarounds because of bugs, edge cases not working, thin or incomplete documentation, inconsistencies, etc.. I guess the V core team is realizing that it takes more time then expected. Originally they planned to be production ready around Christmas 2019. This all sounds more negativ than it is. The community is usually supportive, and V users are currently probably mostly people that like coding and fully aware of the status of V-lang.
On this topic: upgrade V doesn't work on Win10. Neither 'v up' nor 'make'. I always have to delete the V folder and re-install from scratch.
Argh. I didn't create a test case for the V-devs. Really my fault, which why this is last. I had it ones that the program compiled find and run in a panic. It remember it was in a function returning a struct or an error. The program paniced, when I tried to access the struct return. After a while I figured that the function producing the return value, did not return anything in a very specific situation. It took me some time, for then I figured that the function which had several if-then-else statement, did not return anything (no value and no error) in one specific case. Ups. I fixed it and it worked (and I forgot about it :( ). The point is: the compiler didn't complain.
When you create a struct with a ref variable, e.g. struct Ax { b &Bref }
then the compiler complains upon initialising an Ax
if you don't provide a reference. That is very good. Now consider struct MyStruct { a Ax }
. When you do m := MyStruct{}
, implicitely an 'Ax{}' is created, and in this situation the compiler does not complain. Instead a NULL reference is created :( , and will have an unpleasant surprise (panic) later on,
Apologies, this one will be a little longer. The use case (problem) first: The rosie compiler needs to generate byte code for different pattern, e.g. char, string, charset, groups and aliases. Rosie is a pattern language and supports multipliers (?, +, *, {n,m}) and predicates (<, >, !). There is a generic byte code pattern that can be applied for predicates and multipliers. Some pattern however benefit from optimized byte code. I seek a V-lang supported design pattern, that delivers easy to read, easy to maintain (no copy & paste) and flexible to modify source code. E.g. in a first implementation, it is fine for every rosie pattern to generate byte code for the generic pattern. Later, and step by step, optimized byte code will be generated for individual rosie patterns.
In Java I would design an abstract base class, which implements the logic for the generic pattern. And concrete subclasses for the specialisations. These concrete subclasses would first be empty, inheriting everyhting from the base class. The abstract base class typically consists of a single entry method, which calls severals other object methods, which may again call other methods. Which allows subclasses to remain small and clean, by overriding only the few methods needed, to generate the optimized code. If the generic code must be adjusted, you only need to modify the abstract base class. Very easy, readable, maintainable and yet flexible.
I know that V-lang has no inheritance, so that very design pattern cannot be applied. But what would be a V-lang compliant design pattern, that delivers source code which has equally good attributes.
My current V-lang approach uses separate structs per rosie pattern. Attached methods implement the business logic to generate the byte code. The caveat are: it involves copy & paste, and every change to the generic code portions must be carefully copied to all other structs.
I don't want to be picky or nasty, but I thought I write down just in case ...
I think below is a good example where V's syntax could be a bit improved.
mut ar := rosie.libpath.clone()
ar << os.join_path(home, "rpl")
return ar
I personally like [..libpath, "whatever"],
but libpath.clone().add("whatever")
would also be ok. The latter actually fails, because V has issue to detect or make the clone() result mutable, so that another value can be added.
I can't say I especially like the tweak to use structs for named function parameters. Yes, for me it is a tweak. I would prefer the V-lang syntax spec to be improved to natively support named parameter.
That it's more of a tweak then a feature is also evident by the [params]
(compiler) attribute required to allow for empty parameter lists.
NB: the [params] tag is used to tell V, that the trailing struct parameter can be omitted entirely, so that you can write button := new_button(). Without it, you have to specify at least one of the field names, even if it has its default value, otherwise the compiler will produce this error message, when you call the function with no parameters: error: expected 1 arguments, but got 0.
Maps have fairly recently been added to V-lang. Unfortunately V's assert statement does not yet support it. E.g. assert mymap["abc"] == 123
will print *unknown value*
for the left part if the assertation does not match. But that is not my main point. My main point is that V-lang has no generic means to extend how values are printed. Some interface that structs might implement and which features such as asserts (and possibly others) are leveraging. May be str()
which is already used for string interpolation (formatting).
It is nice and easy to build or run a single executable or shared library, but V has nothing to help with release management. E.g. my little vrosie project has:
- A cli executable
- A shared lib (*.so or *.dll)
- A python module is on my todo list, which might end up in a pyvrosie.so file or something
- May be I want to create an rpm package and publish it
- Create the documentation (may be in different formats: html, man pages, ...)
- Run absolutely all tests to make sure everything is working fine
- A build.v file, which builds, tests and installs all the components whenever a binary package is not available
- Properly tags the revision in git
See https://github.com/vlang/v/issues/12411 for more details. In summary, you cannot do
mut iter := data.my_filter()
for x in iter {
eprintln("x: $x => $iter.pos") // (1)
}
V will make a copy of iter
and update the copy. Currently my only solution is: manually craft the loop yourself
IMHO V-lang is a little inconsistent regarding the use of ?
for calling functions that return an optional. What do I mean with that:
-
x := my_function()?
=> the function returns an optional. If the return value is an error, then return from the current function and pass it on to the parent function.?
means "pass the error on" -
x := my_function() or {..}
=> The error will be handled by the 'or' block. No?
-
if x := my_function() {..}
=> If an error, then continue with the 'else' block. No?
-
return my_function()
=> Whatever my_function returns, pass it on the parent. No?
A simple explanation could be: whenever the optional gets handled within the function, than no ?
. If the optional should be passed on to the parent, then use ?
. The only one that doesn't fit is return
. IMHO ?
should be required in the return context.
I raised a feature request to support "stop test execution after first failure". This is now available, though not yet documented. I personally would also like to see an easy to use command line option.
With latest V 7b72326, you can now do:
VJOBS=1 VTEST_FAIL_FAST=1 ./v test . .
or
VJOBS=1 ./v test -fail-fast .
Doing just ./v test -fail-fast .
also works, but may not do what you expect, since if you do not set VJOBS
explicitly to 1 (sequential execution), then all the tests, that were already started in parallel to the one that failed, will still continue their execution.
We can also make -fail-fast
set VJOBS
to 1 implicitly, if that is what people want, at the cost of making the 2 options slightly interdependent.