Skip to content
Juergen Donnerstag edited this page Feb 1, 2023 · 67 revisions

Welcome to my 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 stupid questions.

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
  • (now available) "\x00", the "\x<hex>" notation is not supported
  • (now available) "\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, but then we need a []byte that does not. Or the len attribute is used and \0 has no special meaning. By means of special functions, e.g. from_cstring(), to_cstring() it might be handled. A CString struct probably requires a lot of source code copy & paste, since V has no String-interface which would allow for multiple different implementations.

Test that a method throws an error

  • if _ := fn_that_may_throw_an_error { assert false }

It also works with if/else like this, and the err value is also available.

res := ""
if res = my_fn_that_raises_an_error() {
    // ok path
} else {
    res = another_function(err)?
}

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.

I'm currently using the last one.

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 with 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 build-script that copies the files. I think I mentioning it already elsewhere, but V has no build-system with pre- and post-processors etc.. It basically is just the compiler.

Array slicing

I like the python approach to use negative indexes such as ar[-10 ..], and more generically, when using a range to automatically make sure that lower and upper boundaries are properly adjusted if needed. For performance reasons, V-lang doesn't have it. Instead you need to do ar[.. math.max(0, ar.len - 10)] or similar.

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))] for it to work.

Update: Since recently, V-lang supports ar#[-10 ..]. Please note the extra # which tells the V-compiler to use another range implementation. An implementation that is similar to Python's approach, and always returns a slice. The slice might be empty though. Please see the V-lang docs for more details. I'm undecided yet whether I like the extra '#'. As mentioned in one of the other entries, I like to think of operators as syntax-sugar for (core) functions. Unfortunately V-lang has no such core functions for either of the range types. Which also means, that ranges are not extensible. They are only available for (core) types which are hard-coded in the compiler.

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 or possible 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

I developed a little memory mapping modul, because we needed 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 use system default as default value for a variable. V prints a warning, which, when generating production code with -prod will fail with an error message. It make the code more readable to add defaults, even when it is a system default.

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 to 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 something 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 I 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 gotcha

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.

Function names can not be keyword, such as match

I wanted to create a function (attached to struct) with the name 'match'. Which currently is not possible, as 'match' is a keyword in Vlang. The compiler will complain. Unfortunately VLang doesn't have a proper grammar file right now (like Zig for example), which IMHO would help remove uncertainty.

Build-in module names can not be used in your own modules

I wanted to add a CLI tool to my app, and I thought 'cli' is a good name for a sub-module under the main module, e.g. 'rosie.cli'. Unfortunately that doesn't work right now. Even though it's only a submodule, the compiler complains, reporting that 'cli' is already a module name. Which is true, vlib comes with a cli module, but in a different parent module. Vlang currently ignore the context (parent) and just compares the last name.

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

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

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. UPDATE: This seems fixed now. I use 'make -tcc' on Windows.

I had it ones that not all return paths are checked

Argh. I didn't create a test case for the V-devs. My fault. I had it ones that the program compiled fine but ran into a panic. I remember it was in a function returning a struct or an error. The program paniced when I tried to access the struct in a return stmt. 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, but then I figured that the function which had several if-then-else statements, 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 functionalities 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 everything 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).

Another gotcha with V assertations: Something like assert "net" in p.main.imports will basically end in an endless loop. The issue seems related to the in operator in combination with maps. Add extra brackets such as assert ("net" in p.main.imports) and it works.

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

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

But there is more: accessing map values. E.g.

  • a := mymap["x"] will return either the value found or the default value of the map value type. It will not panic or throw an error => IMHO this should be fixed
  • a := mymap["x"]? will return an error if not found
  • a := mymap["x"] or { 123 } is also fine to provide a default value

I wish the V's syntax would be more consistent here.

Cli option to stop test execution after first failure

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.

Trouble with pointers (and assertions again!)

A very easy example. Any ideas on what might go wrong here?

struct Inner { str string }

struct Outer { inner Inner }

fn (o Outer) inner() &Inner { return &o.inner }

fn test_ptr() ? {
    outer := Outer{}
    eprintln("${voidptr(&outer)}: ${voidptr(&outer.inner)} == ${voidptr(outer.inner())}")
    assert &outer.inner == outer.inner()
}

Running this test actually succeeds. You may use v -stats to print the output. And you'll be surprised: the printed values for voidptr(&outer.inner) and voidptr(outer.inner()) are different?? How can the assertation succeed, if the references are different?

Well, the assertion does not compare the pointers, but it is a V-lang feature that pointers are automatically de-referenced. Which means that the assertion compares the struct values and not the pointers. You can easily validate this by reviewing the generated C-code v -stats -keepc -showcc -cg ...

Ok, so I changed the assertion to assert ptr_str(voidptr(&outer.inner)) == ptr_str(outer.inner()) and now it fails. That is good and bad. Good, because we are now able to reproduce the problem, and bad, because we still don't know where things go wrong.

A closer look at the generated C-code for fn (o Outer) inner() &Inner { return &o.inner } makes we wonder. The function body copies the receiver o struct?!?!. It doesn't need to, but obviously it is because the receiver is not declared with mut and neither is it explicitely a pointer. The solution: either of the following changes makes the example work as envisioned.

fn (mut o Outer) inner() &Inner { return &o.inner }

fn (o &Outer) inner() &Inner { return &o.inner }

Since in my case it doesn't need to be mutable, I choose the 2nd approach.

My lesson learnt: in doubt immutable means also for structs (not just literals) "make an implicit copy". It doesn't mean "you can easily pass around a reference, as it is guaranteed that it will never be modified"

Quite a nasty issue, and certainly I'll watch out for in the future.

Uniform Function Call Syntax

An interesting idea mentioned in one of the V Discord channels. Unfortunately V-lang is not supporting it.

Explicit module name vs implicit directory

My little Rosie lib allows to combine different parsers, optimizers, compilers and runtimes. Initially I played with different instruction sets for the runtime virtual machine, then a core-0 parser and an RPL based parser, then different RPL dialects requiring additional parsers, and so on. I leverage V's module systems by creating a component directory, e.g. ./parser, and a subdirectory per parser. When I start with a new component, I often copy & paste something existing. Occassionally I forget to update the 'module ...' declarations. The V-compiler will complain of course and usually the root cause is obvious, but it made me think: why do we need the 'module' statement? Why not leverage the directory name as module name. What is the additional value of being able to have multiple different modules within the same directory? Some people may argue that rules for directory names are dependent on the Operating System and the rules are different from module name rules (all lowercase etc.). But directory names can be checked as well. I'm uncertain.

Naming convention for receivers

V's naming convention suggests 1-letter receiver names, e.g. fn (p Parser) my_fn(). I don't have a strong opinion after some time coding a V project, but I slightly prefer python's self. To me, it makes it more obvious.

Const blocks

Strangely the syntax is const ( .. ) and not const { .. }. Vlang is usually using curly brackets for blocks. Except when it comes to const-block, where they are using parenthesis.

'return' issues

I've stumbled upon this issue more then once. Let's say you have an if or match statement (with assignement), then you can not return from within these blocks. The compiler throws an error upon the return statement. The solution is to first declare the variable and assign to it in each switch block. But like below it is not always nice. Initialising a "PatternCompiler" is potentially expensive.

fn (mut c Compiler) compile_elem(pat rosie.Pattern, alias_pat rosie.Pattern) ? {
    mut be := match pat.elem {
        rosie.LiteralPattern { PatternCompiler(StringBE{ pat: pat, text: pat.elem.text }) }
        none { return error("Pattern not initialized !!!") }
    }

    be.compile(mut c)?
}

Raw string inconsistency

You can define raw string literals like r"\n". Raw means that what is in between the quotes will not be unescaped but is taken verbatim. r"\n" == "\\n". It is not a big thing, but ocassionaly it makes the code (string) more readable, which is good. Unfortunately raw string literals are not consistently allowed where string literal are possible. E.g. the following declaration of a map with string key values, raises a compiler error message:

x := {
   r"ab": 1
   r"bc": 2
}

Interesting use of 'match'

A little unexpected to me, the following works: An expression in match

m := rand.intn(100)
x := match true {
  m == 0 {'blah'}
  m == 1 {'brah'}
  m < 10 {'less then 10'}
  else { 'error' }
}

Iterate over enums at comptime

In my little project I needed something like:

$for e in SymbolEnum { 
   symbols.symbols << e.name
}

Unfortunately that is not (yet) available

Structured Concurrency

I like this article about structured concurrency. And I did read V's answer, which basically is "Go has plenty such libs and you can build such a library in V as well". Which is true.

I like this article because it explains how programming evolved from goto to flow control statements and (black box) functions, and that this improved quality, re-use, error handling, testing, readability etc.. Then it goes on to make a similar case for go-like statements and structured concurrency, and how structured concurrency helps to avoid many common concurrency issues.

It is true, a V-lib could be developed. Which I think misses the point a bit. If structured concurrency helps to avoid common concurrency bugs and improves code quality, then it should be the default and promoted in V's documentation as an improvement over Go. Currently V is marketing that is has a goto statement (sorry, a go statement).

[]int vs array[int]

since recently V has generics. But arrays and maps are using an old syntax. For consistency reasons, I think it should be changed.

Scoped context

I fully support his comments: https://github.com/vlang/v/discussions/7610#discussioncomment-4784873

Clone this wiki locally