Skip to content
Juergen Donnerstag edited this page Nov 12, 2021 · 67 revisions

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-chars in strings

  • "\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.

Test that a method throws an error

  • 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()?
}

Inconsistent return error("..") behavior in 'if' and 'match'-expressions

  • '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("..")
   }
   ...
}

Libraries and modules

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.

Array slicing

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.

Assert with message

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.

test fails but no output

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.

Inconsistent naming conventions for structs

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.

Inconsistency in import

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.

string, byte and arrays are limited to 2GB entries

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.

vfmt

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.

range, at, iterator conventions

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

Catch panic in test code

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.

Smart-cast and arrays

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

upper_case for structs but snake_case for functions, and no constructor

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 :(

pub mut ...

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...

Array traps

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.

Sumtype cotcha

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.

Macros / Compile time code generation / extendable built process

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.

String literals

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(..)

Array []! and []!!

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

Types and attached methods

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.

str()

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.

typeof(obj) vs obj.type_name()

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.

Benchmarking

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

  1. compile your program: ./v -cc gcc-10 -g -keepc examples/path_tracing.v
  2. run it under valgrind: valgrind --tool=callgrind ./examples/path_tracing
  3. that will produce a callgrind.out.PID file
  4. 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

  1. compile your program (same as before)
  2. heaptrack ./examples/path_tracing
  3. that will produce a heaptrack.path_tracing.PID.zst file
  4. visualise it with heaptrack --analyze heaptrack.path_tracing.PID.zst

afaik, it is also a linux only tool

Compiler maturity

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.

I had it ones that not all return paths are checked

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.

Structs and uninitialized references

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,

Common functionalialities and specialisations

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.

Little syntax improvements

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.

Trailing struct literals

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.

Assert statements

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

Build system

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

Iterators: next() is cloning the object and not allowing access to the iter object

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

Slight inconsitency use of ?

IMHO V-lang is a little inconsistent regarding the use of ? for calling functions that return an optional. What do I mean with that:

  1. 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"
  2. x := my_function() or {..} => The error will be handled by the 'or' block. No ?
  3. if x := my_function() {..} => If an error, then continue with the 'else' block. No ?
  4. 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.

Clone this wiki locally