On June 24th, the V programming language got open-sourced. It’s a new language that’s strongly influenced by Golang and Rust, but takes a more strict approach. There are no global variables, no null or undefined, no object orientation. It promotes pure functions and defaults to immutable variables. In this post, we will learn about the basics of the language and what makes it stand out.

It’s important to note however, that V is in a very early pre-alpha stage. Most of the language is already usable, but some parts are not yet completely implemented1. This will take its time because there is only a single developer (Alexander Medvednikov) behind the scenes. I found V very interesting nonetheless, so I decided to play around with it in order to get a feeling for what the language will be like. All the code you see in this post is fully functioning at the current state of V.

Let’s get started!

At the moment, the only way to install V is to compile it from sources: Clone the repo using git clone https://github.com/vlang/v. Enter the compiler directory using cd v/compiler and start compilation by running make.

Update: As whoizit pointed out, the Makefile is now located in the root of the repo. So you just rave to run cd v; make.

This will download an old version of the V compiler from Github and use it to compile V from sources. You can then run sudo ln -s [path to v repo]/compiler/v /usr/local/bin/v to make the V compiler available globally.

To check if everything worked, run v. This will open up the REPL, which you can use to follow along with this post. If you want compile a V program, just run v my_file.v. With v run my_file.v, your file will get directly executed. So now that V should be running on your machine, let’s have a look at some code!

Naive Fibonacci: functions, entry points, type notation

fn fib(n int) int {
  if n < 2 {
    return n
  }

  return fib(n - 1) + fib(n - 2)
}

This function implements the well-known Fibonacci sequence. As you can see, V is a typed language. Just like in Go, parameter types are denoted using a space as the delimiter. A function’s return type is annotated in the same way. If you omit it, the function’s return type is assumed to be void. Note that (just like in Go), there are no parentheses around the condition of an if-statement.

fn main() {
  for n := 0; n < 40; n++ {
    fib_n := fib(n)
    println('$fib_n')
  }
}

The main function is used as the entry point for a V program.

Update: You can also omit it. Then your top-level-code will be the entry-point, as whoizit pointed out.

Inside our entry-point, there’s the next similarity to Go: While there’s no while keyword2, the for structure is used for all loops. The only way to create a new variable is by using the := operator, so you cannot declare a variable without assigning it a value. Thus, there is no null-equivalent in V.

Greeter: Structs, receiver functions

Besides the basic types like bool, string, byte or int, V allows you to create a so-called struct.

struct User {
  first_name string
  last_name string
}

This is similar to Go’s type User struct { ... } definition or data classes in Kotlin.

Just like in Go, this is not to be confused with a class definition. Although it is not possible to have methods on a struct, you can define so-called “receiver functions”. These are special functions that take a struct besides their normal paremeters:

fn (u User) greet() {
  println('Greetings, $u.first_name!')
}

A receiver function can then be called on the respective structs using dot-notation:

fn main() {
  alex := User{
    first_name: 'Alexander'
    last_name: 'Medvednikov'
  }

  alex.greet() // => 'Greetings, Alexander!'
}

HTTP and JSON decoding: Error Handling, arrays

V features built-in support for both HTTP calls and JSON decoding. In order to use it, you need to import the respective modules.

import http
import json

You can then make HTTP requests very easily:

http.get('https://jsonplaceholder.typicode.com/albums')

If you’re working with a JSON API, you’ll need to decode your response. For that to work, you first need to define a struct that matches the structure of your JSON.

struct Album {
  userId int
  id int
  title string
}

You can then decode the response by using json.decode:

fn get_albums() []Album {
  response := http.get('https://jsonplaceholder.typicode.com/albums')
  parsed_albums := json.decode([]Album, response) or {
    return []Album{}
  }

  return parsed_albums
}

What’s interesting about this is the block behind json.decode: Because the return type of this function is of the so-called option type, it needs to be handled using the or-keyword. It basically means: If there is an error while decoding the response, execute the code in the or-block. In this case, if there is an error, it should just return an empty array of Albums.
You may ask yourself “Why don’t you need the or-Block when doing HTTP requests? They can fail as well, right?”. At least that’s the question that came to my mind when I saw this, and I assume this behaviour will change in a future iteration of the language.

The option type is comes from from the functional programming domain, where it has been used to eliminate null-issues for a long time. In V, it aims to replace Go’s convention of using an error value to denote wether an operation failed3:

// this is the way it works in Go:
result, err := unsafe_function()
if err != nil {
  // ... handle your error
}
// this is the way it works in V:
result := unsafe_function() or {
  // ... handle your error
}

Memoized Fibonacci: Mutability, Global values

You may remember the naive Fibonacci implementation from above. For big numbers, it runs incredibly slow because it repeats a lot of computations. You may know that this can be sped up a lot by using a technique known as memoization:

// this is JS

const cache = [];

function fib(n) {
  const isInCache = cache.length > n;
  if (isInCache) {
    return cache[n];
  }

  const fibN = n < 2
              ? n
              : fib(n - 1) + fib(n - 2);
  
  cache.push(fibN);

  return fibN;
}

In most languages, you can just use a global variable like cache to store results of fib that are already computed. In V, this is not possible because there are no global variables! Thus, our cache needs to somehow be passed into the fib function. One way to achieve this is to define a custom struct Cache that contains an array of already computed values:

struct Cache {
  mut:
    values []int
}

In this definition, the mut keyword means mutable and allows the values array to be mutated. This is required because V treats immutability as the default, remember?

We can now define a new function that takes a Cache value in addition to the n parameter. That way, the cache is injected into the function and you don’t need a global variable.

fn fib_cached(n int, cache mut Cache) int {
  is_in_cache := cache.values.len > n
  if is_in_cache {
    return cache.values[n]
  }

  fib_n := if n < 2 {
    n
  } else {
    fib_cached(n - 1, mut cache) + fib_cached(n - 2, mut cache)
  }

  cache.values << fib_n

  return fib_n
}

What’s interesting here is that function parameters need to be annotated as mutable and you also need to use the mut keyword when calling such a function. This is quite verbose, which I assume is intentional to make all mutations happening more obvious.

Why is V different?

V takes a different approach than most other languages: Go, JavaScript, Scala, Swift, Java etc. are all multi-paradigm-languages. They all have influences from Functional and Object-Oriented approaches, but because they’re flexible, they allow for varying styles of programming.

In most of these languages (especially in JS), this leads to the following problem: You’ll develop your own style of writing in that language and once you’ve got that down, everything is working fine for you. Problems arise once different developers write similar styles of one language. As an example: I write a rather functional style of JS myself. I try to keep mutation to a minimum, I write pure functions, I try to isolate side-effects. Other developers use JS in a rather object-oriented way, and it works great for them, as well. But once we need to work on each other’s code, we quickly risk shooting ourselves in the feet - because JS is too flexible and allows for different styles of coding which don’t mix well. In the case of V, though, it tries to be more strict about what it allows the developer to do. The best example for this is how it does not allow for global variables4

Another way in that V is different is how it was created: Its creator Alex Medvednikov is being crowd-funded over at Patreon. At the moment, there are 42 individual people who support him to create this language. In order to make sure V is easy to use, he develops multiple projects using V while working on it. One of these is Volt, which is a desktop client for messaging tools like Slack or Skype.

It remains to be seen wether V develops into a fully production-ready language. A lot of people offended Alex, claiming that his plans for the language are not possible and he’s scamming his patrons. I’m not knowledgeable when it comes to the scientifics behind creating a programming language but to the naïve Me, it looks like V is a mix of features that are already present in other languages. Also, decisions like the absence of global variables and null values make V code more easy to reason about, so I don’t see why V’s claims shouldn’t be possible. Anyway: Even if V will never be fully implemented, it serves as an example on how programming can work when we’re forced to give up certain features.

I’m eager to follow the future development of V and I’m sure we will hear a lot more about it. What do you think about V? Let me know in the comments or tweet at me (@skn0tt).

P.S.: Thanks a lot to Ivo Balbaert, who pointed out some typos in this post. I appreciate it! 😄

  1. Some parts are still a bit hacked-together, like the macOS implementation of the HTTP package which just calls cURL under the hood. 

  2. sorry for the bad while-pun 😅 

  3. Originally, the snippet used Math.sqrt for illustration purposes. As @ntrel mentioned, this is not a good example. 

  4. Also, with global variables, there is a clear execution context for each function and you can be sure that the results of a function call only depend on its arguments. If V had global variables, this couldn’t be guaranteed!