The V Programming Language
I have taught some iteration of COS 350 Computer Graphics for around 10years now. Each year, I reevaluate all aspects of the course, including which language I use. I have already used many different languages for different aspects (ex: Java, JavaScript, WebGL/OpenGL, Python, C++, Dart), and I have considered many others (ex: Go, Rust, Haskell, Zig, C#). While all useful languages are equally capable, I have noticed two issues across all the various languages that I've tried or investigated. These issues are:
- The student struggles against the syntax/paradigm of the language, causing the student to focus on the tools rather than the concepts.
- The student's program take a very long time to execute, causing the student to forego exploration of concepts due to lack of time.
Here are a few takeaways from my experiments, thus far:
- Dynamic languages are great for quick hacking, but dead slow to use for anything more complicated than a basic Whitted raytracer.
- Templated programming is a non-starter due to cryptic compiler error messages and unwieldy syntax.
- Strong types are a must, because students will often (ab)use vectors, points, directions, normals, tangents, and colors in very, very wrong ways, leading to bugs that are really difficult to debug. (Am I the only one who has used a non-unit direction or tried transforming a normal like a direction? What does it really mean to add two points together?)
- POD structs, data packed in a simple array, and "object" pools are necessary for production renderers, but terrible for learning as they obfuscate what is actually going on.
- A modest level of operator overloading will expose the equation from a line of code faster than any comment.
- Generally, debuggers are a hinderance; using images as debugging output/trace is very helpful.
- Some functional language features are really useful (ex: no side effects), while others mostly are not (ex: list comprehension).
- True multithreading is extremely useful for the advanced students.
- Reflection features can be useful, but built-in JSON/serialization support is prime.
- Languages that take longer than 5min to set up a working build environment or IDE are out.
- Languages that require downloading multiple packages are out.
- Languages with compilers or JITers that aren't basically "instant" are out.
I have yet to find a programming language that has simple syntax, is hackable yet strongly typed, has a compiler/interpreter that produces helpful error and warning messages, and is fast. However, late in 2022 July, I came across a programming language that seems to tick many of the boxes that make it a potential candidate for COS 350. This language is called, V.
According to the main site, the V programming language is "Simple, fast, safe, compiled". The site goes on to say that V...
- is as fast as C
- is bootstrapped
- compiles fast
- compiles to native binaries without dependencies
- avoids doing unnecessary allocations
- has garbage collection, but without the heavy tracing
- can interop and transpile with C
- can cross compile to other systems (Linux, Windows, and kind of OSX)
- can be translated into Javascript (WASM)
- supports hot code reloading
- implements a cross-platform drawing library
- has REPL (Read, Evaluate, Print, Loop)
- ...
Notes:
- I won't use all of the features stated on the landing page, but it's nice to know that V has progressed beyond a toy language since it became open source is July 2019.
- I ran into many bugs in the compiler when creating the projects. The language is actively developed, so be sure to run
v up
frequently to update with latest changes.
Quick Notes
The docs, math module, and rand module contain lots of examples that will get you programming in the V language, but below are a few notes:
- V is not an object-oriented programming language. However,
struct
s can have associated functions that act very much like methods, except with an explicitself
/this
. To simplify notation below, I will refer to these as methods. - V has very limited binary operator overloading (ex:
+
,-
). The limitation is: the types of both operands (variables on either side of operand) must be the same. This means that you can add aVector2
and aVector2
, resulting in aVector2
, or subtract aPoint2i
from aPoint2i
, resulting in aVector2i
. However, you cannot add aPoint2
and aVector2
(which would be aPoint2
), becausePoint2
andVector2
are different types. - V distinguishes between variable declaration+initialization (
:=
) and assignment (=
).- You can only declare+init a variable once in a scope (within tightest enclosing
{
and}
) - You can only assign to a variable if it was declared as mutable (
mut
).
- You can only declare+init a variable once in a scope (within tightest enclosing
- V has two range operators:
..
and...
. The...
operator includes the second operand while..
does not. For example,0 .. 2
will be a range over0
and1
only, while0 ... 2
will be over0
,1
, and2
.- note:
for
does not work with...
but does with..
- note:
match
works with...
and..
(link) - note:
...
is also used to expand a struct into another for struct updating and to handle variable number of arguments.
- note:
- Looping is done with
for
only. V'sfor
loop is quite versatile. For example:for { /* ... */ }
will loop forever (unconditionally) unless there is abreak
, similar towhile True
orwhile(true)
in other languagesfor x < 100
will loop whilex
is less than100
, similar towhile x < 100
in other languagesfor x in 0 .. 5
will loopx
over range0
,1
,2
,3
,4
, similar tofor(x = 0; x < 5, x++)
in other languagesfor v in ["foo", "bar"]
will loopv
over values"foo"
and"bar"
- However, V's
for
does not work with...
The gfx
Module
The gfx
module provides lots of functionality that will be used throughout the COS 350 course.
I split up the module across several files to provide some categories for finding needed functionality.
Note: To simplify the code, I have chosen to make nearly all of the struct
fields to be immutable.
This decision means that many struct objects will be created, but it also means that the code will be much cleaner as you will not need to add mut
to function arguments.
The exceptions to this are with Image
, Image4
, Color
, and Color4
structs.
Maths
The maths_1d.v
, maths_2d.v
, and maths_3d.v
files contain struct
s, const
s, and fn
s that deal with mathematics in 1D, 2D, and 3D, respectively.
In general, if a struct ends in i
, then the field types are int
s.
Otherwise, the field types are f64
s, double precision floats.
The 1D functions are min3
, max3
, and int_in_range
.
min3
and max3
are convenience functions that return the minimim or maximum value (resp) among the three passed numerical arguments (ex: u8
, int
, f64
).
int_in_range
is a wrapper for rand.int_in_range
, returning an int
chosen uniformly at random between between the two passed int
arguments.
The 2D struct
s are as follows.
Point2i
andPoint2
are points in 2D space, where the types of thex
andy
coords areint
orf64
, resp.Vector2i
andVector2
are vectors in 2D spaceSize2i
andSize2
are 2D sizes, with awidth
andheight
LineSegment2i
represents a line segment with twoPoint2i
endpoints
The point2i_rand
function acts like a constructor that creates a Point2i
with coords chosen uniformly at random between the two given Point2i
arguments.
The 3D struct
s are as follows.
Point
,Vector
,Direction
, andNormal
represent points, vector, directions, and normals in 3D space. The types for all coordinate (x
,y
,z
) aref64
.Ray
represents a 3D ray, which has an origin (point) and direction. Additional fields are available to "control" the validity of distances along the ray.Frame
andLookAt
are two different representations of coordinate frames. You will likely not useLookAt
directly, but instead you will convert it to aFrame
.
Several "constructor" functions are available for creating struct
s that have certain properties.
For example, all directions and normals should have unit magnitude/length (although, technically they don't have lengths).
As another example, the axes of a frame should be orthonormal.
The constructors help provide these guarantees.
Vectors have methods for computing different norms or lengths (ex: \(\ell^1\), \(\ell^2\), \(\ell^\infty\) norms, see Vector Norm)
Points have methods for computing averages and linear interpolated (LERP) points.
There are methods for performing arithmetic operations (add, sub, scale, negate, dot, cross, etc.) on the struct
s.
There are conversion methods for converting from one type to another (ex: convert Direction
to Vector
, LookAt
to Frame
) and convenience methods that provide shorthands for common computations (ex: computing the vector from one point to another).
Image and Color
The image.v
and color.v
files contain lots of functionality for producing, loading, and saving images.
There are two main types:
Image
andColor
deal only with red, green, and blue channels (RGB).Image4
andColor4
also adds an alpha channel (RGBA).
If you make changes to the code relating to Image
or Image4
, note that the color data of an image are stored in row-major 2D arrays of Color
or Color4
.
Note: y increases down the image.
Two functions, generate_image0
and generate_image1
, will be used in the first project.
They demonstrate how to create, modify, and return an image.
The image.v
file contains three functions, load_image
, load_image4
, and save_image
, that handle reading/writing an image from/to a file in the NetPBM format.
See NetPBM notes for details on the format.
Scene and Intersection (Raytracing only)
The scene.v
file contains struct
s and an enum
to represent different aspects of a scene for raytrace rendering.
The struct
s are as follows.
Scene
is the root data structure for everything with the scene to be rendered.Camera
holds the extrinsic details about the rendering viewpoint, i.e., the camera's world positionSensor
holds the intrinsic details about the camera, such as the "physical" size of the sensor, the resolution density of the sensor, and the distance between the sensor and focal point.Light
is single source of lighting for the scene.Surface
is a single renderable surface object in the scene.Material
holds the reflectivity details of a surface. In other words, it describes how light interacts with the surface.
The single enum
gives a name to two different surface shapes: sphere
(0
value) and quad
(1
value).
Note: When representing enum values in JSON, you will need to specify using the corresponding int
values.
The scene.v
file also contains a few convenience getter functions, and functions/methods for loading scenes from a JSON file.
Note: the camera's frame is oriented so that x increases right, y increases up, and z increases backwards.
The intersection.v
file contains a struct
for holding details about an intersection.
There are several functions and methods to help.
Note: the fields of Intersection
could be an optional type (?Surface
) to indicate a hit/miss, but making this change causes the readability of the code to drop significantly.
Instead, use an infinite distance (distance=math.inf(1)
) to indicate a miss, and a finite value (ex: distance=42.0
) to indicate a hit.
Pipeline (Pipeline only)
The pipeline.v
file contains struct
s to represent data in different parts of graphics pipeline.
The struct
s are as follows:
Vertex
hold position (point
) and color (color
) of vertices that define a shape.GeoPrimitive
are the basic geometry primitives of our pipeline, which are line segments (in OpenGL and other pipelines, the primitive is a triangle).Fragment
is the simplest renderable object. In our pipeline, a fragment covers a pixel.
This area is still under construction!
Some Issues with V
While I have enjoyed working in V, I have run into many issues that required weird workarounds for problems or lots of comments on how to use. Below are a few issues I've discovered.
Note: some of these issues have been addressed by the V devs! Sadly, I have not yet updated the list below due to these changes...
- Even after playing with them for a while, optionals (
?
) and sum types (|
) are still unintuitive. Embedded structs are useful, but I frequently use the compiler/docs to iterate over the code to get it to work right. - Compiler warnings can be inconsistent sometimes. For example, setting default value for a
struct
field that is the same its "zero" value produces a warning that the default value isn't needed (ex:struct Foo { bar bool = false }
vsstruct Foo { bar bool }
), while empty arrays without empty curlies (ex:arr := []u8
vsarr := []u8{}
) warns that curly braces should be there. - No way to declare a variable and type without giving it an initial value. Any actual initial settings require making it mutable with
mut
. It is possible to useif
expressions, but this gets really awkward very quickly. - Sometimes working with multiple values requires parantheses and other times they must not be there. For example, in
fn foo() (int,int) { return 42, 24 }
, the return types must be in parens but the returned values cannot. - No modifier for private and non-mutable, only for public and or mutable (
pub:
,mut:
,pub mut:
). - Private fields cannot be directly accessed from anothor module, but they can still be initialized! This means there is not a way to protect initialization of struct fields or to provide any guarantees about the fields' values. For example, there is no way to guarantee a direction struct has unit length.
- I still have to guess about when to use
:
and when to use=
in some situations. - No function overloading?? :(
- Operator overloading is there, but it is very limited and buggy.
- Caution: I ran into many bugs in V when using the
+=
operator! Everything worked fine if I explicitly broke the code into separate=
and+
(which is what V should be doing under the hood...)
- Caution: I ran into many bugs in V when using the
- Using structs for optional function arguments is an ugly hack in my opinion, but it's (somewhat) consistent with the language.
- Adding
mut
modifier to astruct
field or function argument has cascading effects. (similar toconst
in C++, andasync
in most languages) - Custom iterators feel like a hack.
- Returning
error()
when done iterating. - Must be assigned to variable.
- Must do:
iter := Iterator{...}
andfor v in iter {...}
- Cannot do:
for v in Iterator{...} { ... }
- Variable does not need to be declared
mut
?
- Must do:
- Returning