Denim's Language Design Takes

Programming language design opinions sorted more or less from hottest (most unique/surprising) to coldest (Least unique/surprising).

Hot Takes

Concurrency

In a typical concurrency regime, like async/await, programs can "opt in" to asynchrony in specific spots. Additionally, special functionality like Promise.all(...) is necessary to express the idea that an operation should wait for multiple prior operations to complete before it can begin.

Denim takes a very different approach: in Denim, concurrency is a first-class syntactic concern.

Firstly, all operations in Denim are "awaited" automatically. When you don't need to wait for something to complete, you can let in complete without blocking using the async keyword.

#![allow(unused)]
fn main() {
quick_operation_1();

// We wait for `quick_operation_1` to complete before starting
// `quick_operation_2` without having to use a keyword - just works.
quick_operation_2();

let eventual_value = super_slow_operation().async;
thing_that_needs_to_happen_right_away();

print("it's ready now: ${eventual_value.await}");
}

Secondly, and perhaps most importantly, Denim allows the programmer to ergonomically stipulate which things should happen at the same time, and which should happen in sequence.

Statements separated a blank line are executed in sequence.

#![allow(unused)]
fn main() {
let greeting = "hello";

// Performs a network call to figure out what planet we are on.
let planet = get_current_planet();

// Waits for a 3 second time to elapse.
wait_for_3_seconds();

// Nothing is printed to the screen until 3 seconds **after** `planet` is
// fetched.
print("$greeting, $planet!");
}

Statements not separated by a blank line are executed concurrently.

#![allow(unused)]
fn main() {
let greeting = "hello";
let planet = get_current_planet();
wait_for_3_seconds();

// We print the moment all 3 of the statements in the previous group complete.
print("$greeting, $planet!");
}

The result is a terse yet readable way to sequence asynchronous logic.

Fluency

In many lanuages, lots of the good stuff is only usable when invoking a function as a member of a thing. Take for instance ?. in JavaScript.

const foo = Math.random() > 0.5 ? { bar() {} } : null;

foo?.doSomethingMaybe(); // this is dope

function baz(foo) {
  // ...
}

// I need this grossness to **safely** call `foo`:
if (foo) {
  baz(foo);
}

This seems silly. ?. is one example of the fact that we humans love to create sequential, causal chains of things to do. Breaking these chains with control flow like if does not feel good.

Denim is designed to make "chaining" operations as ergonomic. In a sense, Denim takes Rust's .await syntactic concept to its logical conclusion: let's make everything that can be used as a "prefix" usable as a "suffix".

#![allow(unused)]
fn main() {
fn baz(foo: Foo, scalar = 1.0) -> double {
  // ...
  scalar * 123
}

foo.baz(foo: it, scalar: 2.0).if it < 200 {
  print("it is $it!")
}
}

Denim accomplishes a language feature called "fluency" by:

  • Allowing control flow keywords to be suffixed like .try and .if
  • Supporting the it keyword which is the value of the preceding expression in the "chain"

"DSL"-ability

Denim is designed to make library APIs easy on the eyes. It accomplishes this via two language quirks:

  1. Functions with zero arguments may be invoked without a trailing ()
    This allows functions to behave like properties, encapsulating complexity the same way getters do in other languages.
    #![allow(unused)]
    fn main() {
    fn forty_two() -> int { 42 }
    
    fn ten(to_the_power_of = 1.0) -> int { (10 ** to_the_power_of).round() }
    
    print(forty_two * ten) // Prints "420"
    }
  2. Functions can have a special parameter block of type fn() -> T that adds aesthetically pleasing syntax sugar\
    #![allow(unused)]
    fn main() {
    fn foo(a: string, block: fn() -> string) -> string {
      "a $block"
    }
    
    foo("hello") {
      foo("darkness") {
         "my old friend"
      }
    }
    }

2+ function parameters must be explicitly labeled

Denim requires that parameters are labeled when 2 or more parameters are included in a function invocation.

#![allow(unused)]
fn main() {
fn add(a: int, b = 0) -> int {
  a + b
}

add(1) // compiles

add(1, 2)       // does not compile
add(a: 1, b: 2) // compiles
}

When all you need is positional arguments, consider a tuple or array.

#![allow(unused)]
fn main() {
fn add(nums: (int, int?)) -> int {
  nums.0 + (nums.1 ?? 0)
}

add((1, 2)) // compiles
}

Medium Takes

Imports at the bottom of the file

This is normal:

import stuff up here

yada yada yada

maybe some exports

mhm yeah

**finally** the stuff you came here to read

Denim puts imports and exports a the bottom of the file after the logic and stuff.

#![allow(unused)]
fn main() {
pub type ImportantStuff = struct {
  // ...
};

---
from "github.com/some/lib" use something;

from "~/internal/lib" show something_else;

}

No > or >= comparison operators

Why use > or >= when < and <= do trick?

No bitwise operators whatsoever

Denim does not have bitwise and, or, zor, and not. Why? Most logic doesn't use these operators. Logic that needs to do bitwise math should use good ol' fashioned functions. Good riddance.

and and or instead of && and ||

Pretty much only Python does this, but I think it reads nicely and reduces parsing ambiguity (|| could be the beginning of a lambda).

Everything is an expression

Like in Rust, most things in Denim are expressions. This means they yield a value. ; is used to turn an expression into statement. The value of a statement is ignored.

#![allow(unused)]
fn main() {
print(if value > 8 { "pretty big" } else { "not that big" });
}

Spaces instead of tabs

Yeah. 2 space indent. Deal with it 😎.

Cold Takes

Rust-style enum

Denim steals Rust's enums because they are super expressive while remaining practical and readable.

#![allow(unused)]
fn main() {
enum Take {
  Hot { is_outta_pocket: bool },
  Medium,
  Cold(temp: f32),
}
}

Rust-style pattern matching

Denim steals Rust's pattern matching because it gets a lot right.

#![allow(unused)]
fn main() {
match number {
  // Match a single value.
  1 => print("One!"),
  // Match several values.
  2 | 3 | 5 | 7 | 11 => print("This is a prime"),
  // Match an inclusive range.
  13..=19 => print("A teen"),
  // Handle the rest of cases.
  _ => print("Ain't special"),
}

// Match is an expression too
let binary = match boolean {
  // The arms of a match must cover all the possible values
  false => 0,
  true => 1,
};
}

The only thing it was missing is being able to eaily match a single arm in a if statement:

#![allow(unused)]
fn main() {
if thing is Some::EnumVariant {
  print("Bingo!");
}
}

Dart-style string syntax

Dart makes declaring, concatenating, and interpolating values within strings super easy. Denim steals (most) of this syntax. A notable exception Denim string literals use " instead of '.

let abc = "123""xyz" // concat just by putting literals next to each other.
let multiline = """
  take
    all
      the
        space
          you
            need
""";

let a = 1;
let b = 2;

let c = "$a + $b = ${a = b}" // Use `$` for string interpolation!