The Vale Programming Language

Normally, our code needs to know the exact type of a struct to be able to work with it.

In this example, hopscotch only works for a Human.

If we want a hopscotch for DarkElf too, we'd have to copy and paste the hopscotch function and adjust it to use DarkElf instead. How tedious!

It would be great if we could make a function accept any kind of reference, as long as it has the right functions (such as hop and skip).

We can, using interfaces!

vale
struct Human { ... }
func hop(s &Human) { ... }
func skip(s &Human) { ... }

func hopscotch(s &Human) {
s.hop();
s.skip();
s.hop();

}


struct DarkElf { ... }
func hop(s &DarkElf) { ... }
func skip(s &DarkElf) { ... }

exported func main() {
wulfgar = Human(...);
hopscotch(&wulfgar);

}

Using Interfaces

We can create an interface with the interface keyword, like Bipedal here.

Then, to tell the compiler that Human and DarkElf conform to that interface, we use the impl keyword. 0

Last, we change hopscotch to take in a &Bipedal, and now we can call it with a Human or DarkElf, since both implement the Bipedal interface.

vale
interface Bipedal {
func hop(virtual b &Bipedal) void; 1
func skip(virtual b &Bipedal) void;

}

struct Human { ... }
func hop(h &Human) { ... }
func skip(h &Human) { ... }
impl Bipedal for Human;

func hopscotch(b &Bipedal) {
b.hop();
b.skip();
b.hop();

}


struct DarkElf { ... }
func hop(s &DarkElf) { ... }
func skip(s &DarkElf) { ... }
impl Bipedal for DarkElf;

exported func main() {
wulfgar = Human(...);
hopscotch(&wulfgar);
drizzt = DarkElf(...);
hopscotch(&drizzt);

}

Side Notes
(interesting tangential thoughts)
0

When a struct implements an interface, we call it a substruct of that interface.

1

Future versions will have a much simpler syntax, see the bottom of the page for a preview!

Downcasting

We can downcast an interface to a struct using the as function:

vale
interface Ship { }

struct FireflyShip {
name str;
}
impl Ship for FireflyShip;

exported func main() {
// Implicitly upcast to Ship
ship Ship = FireflyShip("Serenity");

// Can downcast with as:
fireflyShip = ship.as<FireflyShip>().expect();
println(fireflyShip.name);

}
stdout
Serenity

Note that as<FireflyShip>() returns a Result<FireflyShip, Ship>, which will be either an Ok containing a FireflyShip, or an Err containing a Ship. In the above code, we're calling .expect() because we assume it is indeed a FireflyShip.

Open Interface Constructors

(Also known as "Anonymous Substructs")

When we make an open interface like the above Bipedal, the compiler makes an open interface constructor, a function which can define another substruct.

It takes functions (or lambdas) as arguments, to use for the interface's methods.

In this example, we're giving it a lambda for its launch method.

vale
import stdlib.*;

interface IShip {
func launch(virtual ship &IShip);

}

exported func main() {
x = IShip({ println("Launching!"); });
// x is an unnamed substruct which
// implements IShip.

x.launch();

}

Planned Features

These are planned features for Vale. See the roadmap for plans!

Simplified Syntax

The current syntax is closer to the way Vale internally thinks about interfaces. However, an easier syntax is planned:

  • The methods could be inside the interface.
  • We could use self instead of virtual self &Bipedal
  • We could have the impl inside the struct.
  • We could have override methods inside the impl.

interface Bipedal {
  func hop(self) void;
  func skip(self) void;
}

struct Human {
  ...

  impl Bipedal {
    func hop(&self impl) { ... }
    func skip(&self impl) { ... }
  }
}

Sealed Interfaces

By default, an interface is open, which means any struct can implement it.

A sealed interface can only be implemented by structs defined in the same file.

vale
interface MaybeInt sealed { }
// sealed: only the below two
// structs can implement MaybeInt.

struct NoInt { }
impl MaybeInt for None;

struct SomeInt { value int; }
impl SomeInt for MaybeInt;

Because the compiler knows all the implementing structs up-front, it:

  • Can be inlined. 2
  • Checks that match statements consider all substructs.
  • Generates sealed interface constructors.

2

Locals and members can inline sealed interfaces. The compiler will figure out the size of the largest substruct, and reserve that much memory in the stack (if an inl local) or the containing struct (if an inl member), and use that memory for the substruct. (This is the equivalent of a C/C++ union or Rust enum)

Sealed Interface Constructors

A sealed interface constructor is a function with the same name of the interface, which constructs the correct subclass depending on the given arguments.

In this example, when we call MaybeInt(7), the compiler finds that the SomeInt substruct indeed has a constructor accepting one int, and calls that.

vale
// Using above MaybeInt/NoInt/SomeInt

exported func main() {
x = MaybeInt(7);
// x is of type SomeInt.

y = MaybeInt();

// y is of type NoInt.
}

A sealed interface constructor also allows us to make substructs via shortcalling. 3

3

See Structs for more on shortcalling.

Shortcalling Sealed Interface Constructors

We normally call a function by name, such as the MaybeInt(7) above. However, if the code is expecting a certain type, it can automatically call the constructor of the expected type.

vale
// Using above MaybeInt/NoInt/SomeInt

exported func main() {
// These statements are equivalent:
x MaybeInt = MaybeInt(7);
x MaybeInt = (7);

}

This saves a lot of typing when calling functions.

vale
// Using above MaybeInt/NoInt/SomeInt

func foo(m MaybeInt) { ... }

exported func main() {
foo(SomeInt(7));
// Equivalent, calling sealed
// interface constructor:
foo(MaybeInt(7));
// Equivalent, shortcalling sealed
// interface constructor:
foo((7));

foo(NoInt());
// Equivalent, calling sealed
// interface constructor:
foo(MaybeInt());
// Equivalent, shortcalling sealed
// interface constructor:
foo(());

}

Next: Generics