LanguagesArchitecture

Something unexpected happened, once we added const generics to the 0.2 beta.

We discovered that by enabling passing functions as generic parameters, we also effectively enabled concepts, a way to specify constraints on parameters, without making them implement any traits.

Behold, a Concept Function!

For example, let's say we already have a Ship struct and a calcDamage function:

vale
struct Ship {
strength int;
}
func calcDamage(ship &Ship, target &Ship) int {
ship.strength - target.strength
}

Now we want a battle function, which can take any type as long as it has a calcDamage function:

vale
func battle<T>(attacker T, defender T)
where func calcDamage(&T, &T)int
{
damage = calcDamage(attacker, defender);
// ... other logic
return damage;

}

There it is! That where func calcDamage(&T, &T)int specifies that there must be a calcDamage function that takes in two &Ts.

The only existing language today which can accomplish something like this is C++, with its requires requires clause:

c++
template<typename T> requires requires(T a) { { calcDamage(&a, &a) } -> std::same_as<int>; } int battle(T* attacker, T* defender) { int damage = calcDamage(attacker, defender); // ... other logic return damage; }

As you can see, Vale makes it wonderfully easy to use this approach.

We already use it thoroughly in our standard library. For example, in optutils.vale, we have a function Opt<T>.clone that's only enabled if T also has a clone method:

vale
func clone<T>(self &Opt<T>) Opt<T>
where func clone(&T)T
{
...
}

This can be a lot easier, compared to previous approaches.

Previous Approaches, Traits and Interfaces

For example, in Java or C# or Rust, if we had a pre-existing Ship and calcDamage function, we would have to make an interface (or trait) to describe the bounds and then require all callers to to make their arguments extend that interface (or make an impl, in Rust's case) for every type that they want to supply.

Here's an example in Rust:

rs
// Let's say we had a pre-existing function and a struct... struct Ship { strength: i32 } fn calcDamage(ship: &Ship, target: &Ship) -> i32 { return ship.strength - target.strength; } // And we want a function that can take any type. // We'll need a trait for the functions we want to call on it. trait Fireable { fn calcDamage(&self, target: &Self) -> i32; } fn battle<T: Fireable>(attacker: &T, defender: &T) -> i32 { let damage = attacker.calcDamage(defender); // ... other logic return damage } // And the caller must make an `impl` for every type we want to feed into `battle` impl Fireable for Ship { fn calcDamage(&self, target: &Self) -> i32 { // (Can optionally inline this, if this is the only usage.) return calcDamage(&self, &target); } }

If we can't modify the existing type (such as if it's defined by a third-party library) we sometimes need to make a wrapper class (sometimes known as a typeclass or a newtype) which can implement the required interface.

Unexpected Déjà Vu from C

After using this for a few weeks, I had a shocking realization: this is similar in spirit to how we did things in C!

This snippet in Vale...

vale
struct Ship {
strength int;
}
func calcDamage(ship &Ship, target &Ship) int {
ship.strength - target.strength
}


func battle<T>(attacker T, defender T)
where func calcDamage(&T, &T)int
{
damage = calcDamage(attacker, defender);
// ... other logic
return damage;

}

...would be this in C:

c
struct Ship { int strength; }; int calcDamage(Ship* ship, target &Ship) { return ship.strength - target.strength; } typedef int (*Fire)(void*, void*); int battle( void* attacker, void* defender, Fire calcDamage) { int damage = calcDamage(attacker, defender); // ... other logic return damage; }

The only real difference is that Vale passes the calcDamage function in at compile-time, and C passes it in at run-time via a function pointer. Aside from that, these approaches are the same.

Note how neither requires that calcDamage be a method of the type, they can be free functions. I think this is a much cleaner approach, that allows us to decouple the type from the functions we use on it.

That's all!

Thanks for reading, we hope you enjoyed this article! And if you're impressed with our track record and believe in the direction we're heading, please consider sponsoring us on github:

With your support, we can bring easier generics to programmers worldwide!

Side Notes
(interesting tangential thoughts)

Vale's Vision

Vale aims to bring a new way of programming into the world that offers speed, safety, and ease of use.

The world needs something like this! Currently, most programming language work is in:

  • High-overhead languages involving reference counting and tracing garbage collection.
  • Complex languages (Ada/Spark, Coq, Rust, Haskell, etc.) which impose higher complexity burden and mental overhead on the programmer.

These are useful, but there is a vast field of possibilities in between, waiting to be explored!

Our aim is to explore that space, discover what it has to offer, and make speed and safety easier than ever before.

In this quest, we've discovered and implemented a lot of new techniques:

  • Generational Memory, for a language to ensure an object still exists at the time of dereferencing.
  • Higher RAII, a form of linear typing that enables destructors with parameters and returns.
  • Fearless FFI, which allows us to call into C without risk of accidentally corrupting Vale objects.
  • Perfect Replayability, to record all inputs and replay execution, and completely solve heisenbugs and race bugs.

These techniques have also opened up some new emergent possibilities, which we hope to implement:

  • Region Borrow Checking, which adds mutable aliasing support to a Rust-like borrow checker.
  • Hybrid-Generational Memory, which ensures that nobody destroys an object too early, for better optimizations.
  • Seamless concurrency, the ability to launch multiple threads that can access any pre-existing data without data races, without the need for refactoring the code or the data.
  • Object pools and bump-allocators that are memory-safe and decoupled, so no refactoring needed.

We also gain a lot of inspiration from other languages, and are finding new ways to combine their techniques:

  • We can mix an unsafe block with Fearless FFI to make a much safer systems programming language!
  • We can mix Erlang's isolation benefits with functional reactive programming to make much more resilient programs!
  • We can mix region borrow checking with Pony's iso to support shared mutability.

...plus a lot more interesting ideas to explore!

The Vale programming language is a novel combination of ideas from the research world and original innovations. Our goal is to publish our techniques, even the ones that couldn't fit in Vale, so that the world as a whole can benefit from our work here, not just those who use Vale.

Our medium-term goals:

  • Finish the Region Borrow Checker, to show the world that shared mutability can work with borrow checking!
  • Prototype Hybrid-Generational Memory in Vale, to see how fast and easy we can make single ownership.
  • Publish the Language Simplicity Manifesto, a collection of principles to keep programming languages' learning curves down.
  • Publish the Memory Safety Grimoire, a collection of "memory safety building blocks" that languages can potentially use to make new memory models, just like Vale combined generational references and scope tethering.

We aim to publish articles biweekly on all of these topics, and create and inspire the next generation of fast, safe, and easy programming languages.

If you want to support our work, please consider sponsoring us on GitHub!

With enough sponsorship, we can:

  • Work on this full-time.
  • Turn the Vale Language Project into a 501(c)(3) non-profit organization.
  • Make Vale into a production-ready language, and push it into the mainstream!