Overview
Denim is a comfortable, familiar programming language designed for interoperability.
Pitch
So, you know how pretty much every modern, garbage-collected language feels eerily like the others lately? I think we can blame this phenomenon on the fact that many of these languages are converging on the same features and concepts; how many languages have added first-class functions, co-routines, data classes, and language-level immutablity recently?
The only tangible differences between one language and another are the ecosystems and platforms that they can unlock for you. Go gets you into the cloud and terminal ecosystems, while JS/TS gets you into the browser and to the edge. Swift and Java get you onto phones, and with C# you can ship on an Xbox.
And it got me thinking: if the languages we use to write our apps are this similar, why on earth are we writing the same logic over and over again? Why can't we write most of our logic, constants, and types once, and use them anywhere? What if there was a language purely designed to be interoperable with other languages?
Denim is that language.
The intent behind Denim is to incorporate the smallest set of common features from these garbage-collected languages sufficient to:
- Create common types
- Implement business logic
- Declare common constants
Of course, it wouldn't hurt to end up with a language that is pleasant to use and maintain while we're at it.
Why "Denim"?
Well, because "denim goes with everything" rimshot.
While our slogan is a little playful and intended to earn a few chuckles, I think it accurately embodies the vision of the language. Denim aims to be as comfortable and complementary as any well-worn pair of jeans. This is a language at its best as a part of your outfit (or codebase for that matter).
Design
Denim is not designed to be particularly fast, sexy, interesting, or well-suited for any specific domain. It should fit right into the source code powering any piece of software - from servers to smart fridges. Denim's guiding design principles, are to be maximally:
- Easy to reason about and read
- Pleasing to look at and work with
- Quick to learn and manipulate
Denim should never feel as esoteric and ornate as Rust, but it should feel a smidge more expressive than Go. It should be easy to read, follow, and document like Java, while getting out of your way and letting you practically solve your problem like Node.js.
Inspiration
As Denim is designed to feel familiar, it borrows heavily from some popular programming languages/runtimes:
- Dependency management from Deno
- Module system and batteries-included standard library championed by Go
- Syntax largely stolen from Rust with a few tricks from Dart and Python included
- Extensibility workflow taken from both Dart and Rust
tl;dr "Dart with Rust syntax and Go's packaging model".
Compatibility
For Denim to be useful, in needs to be able to interop with most of the major languages with an established industrial presence. Initially, Denim is being developed with the following transpilation targets in mind:
- TypeScript for web (and also everything else)
- Python for data science
- Swift for Apple's ecosystem
- Kotlin for Google's ecosystem
Thereafter, compatibility could be coming to a friendly neighborhood language near you!
Journal
Here lies all of @skeswa's notes on Denim's implementation and design over time.
Denim
I think Denim needs to be a quite a bit simpler than the original design.
Principles
In order of importance:
- Low-mental overhead
- Aesthetic
- Expeditious
Sub-principles
- Low-mental overhead
- Familiar
- Scannable
- Aggressive complexity containment
- Aesthetic
- Fluent (things ergonomically chain together)
- DSL friendly
- Terse
- Expeditious
- More meaning in less syntax
- Highly extensible
Main Ideas
- Low-mental overhead
- Familiar
- Scannable
- Aggressive complexity containment
- Aesthetic
- Expeditious
- More meaning in less syntax
- Highly extensible
Code Samples
Common-sense builtins
#![allow(unused)] fn main() { let u1: u8 = 1u8; let u2: u32 = 12u32; let i: i32 = 123i32; let i: i64 = 123i64; let f1: f32 = 1.2f32; let f2: f64 = 1.2345e-6f64; let b: bool = false; let s: str = "hello"; let list: [i32] = [1, 2, 3]; let set: [:i32] = [:1, :2, :3]; let map: [str:i32] = ["a": 1, "b": 2, "c": 3]; }
Dart-style doc comments
#![allow(unused)] fn main() { /// This is a doc comment. /// /// These comments are intended to document your code in greater detail. To /// facilitate this greater detail, these kinds of comments have: /// 1. **full** _Markdown_ `support` /// 2. Dart-style `[]` code links /// For example, [abc] references the variable created above explicitly. let forty_two = 42; }
Go-style packaging and visibility
foo/xy.π
#![allow(unused)] fn main() { type X = i32 | str; pub type Y = { a: 1, b: false }; fn x() { print("x"); } fn y() { print("y"); } }
foo/z.π
#![allow(unused)] fn main() { pub type Z = X & Y; // all of foo/xy.π is visible to the whole foo/ directory pub fn z() { x(); y(); } }
bar/b.π
#![allow(unused)] fn main() { pub type B = Y | Z; pub fn b() { z(); } --- from ~/foo use Y, Z, z; // Can only see `pub` decls from `~/foo` }
Rust-style enums
#![allow(unused)] fn main() { // Create an `enum` to classify a notification event. Note how both // names and type information together specify the variant: // `MessageReceived != MessageSent` and `UserJoined(String) != UserLeft(String)`. // Each is different and independent. enum NotificationEvent { // An `enum` variant may either be `unit-like`, MessageReceived, MessageSent, // like tuple structs, UserJoined(String), UserLeft(String), // or c-like structures. Reaction { emoji: char, message_id: u64 }, } // A function which takes a `NotificationEvent` enum as an argument and // returns nothing. fn handle_notification(event: NotificationEvent) { event.match { NotificationEvent::MessageReceived => print("message received"), NotificationEvent::MessageSent => print("message sent"), // Destructure `username` from inside the `enum` variant. NotificationEvent::UserJoined(username) => print("user '{}' joined", username), NotificationEvent::UserLeft(username) => print("user '{}' left", username), // Destructure `Reaction` into `emoji` and `message_id`. NotificationEvent::Reaction { emoji, message_id } => { print("reaction '{}' on message {}", emoji, message_id); }, } } }
Rust-style pattern matching
#![allow(unused)] fn main() { // A function which takes a `NotificationEvent` enum as an argument and // returns nothing. fn handle_notification(event: NotificationEvent) { event.match { NotificationEvent::MessageReceived => print("message received"), NotificationEvent::MessageSent => print("message sent"), // Destructure `username` from inside the `enum` variant. NotificationEvent::UserJoined(username) => print("user '{}' joined", username), NotificationEvent::UserLeft(username) => print("user '{}' left", username), // Destructure `Reaction` into `emoji` and `message_id`. NotificationEvent::Reaction { emoji, message_id } => { print("reaction '{}' on message {}", emoji, message_id); }, } } }
Strict param labeling
#![allow(unused)] fn main() { fn short_story(name: str) -> str { "My name is $name" } print(short_story("Galois")); fn story(age: i32, name: str) -> str { "${short_story(name)} and I am $age years old" } print(story(name: "Galois", age: 20)); print(story(age: 20, name: "Galois")); print(story(20, "Galois")); // Compile time error fn tuple_story((age, name): (i32, str)) -> str { story(age, name) } print(tuple_story((20, "Galois"))); }
Imports at the bottom
#![allow(unused)] fn main() { pub fn my_func(ctx: str) { let something: SomeType = some_func(ctx); let some_value = something.match { SomeVariant => 42, _ => 0, }; let cool_thing: CoolThing = some_value.a_func_from_a_trait_impl(ctx); cooler.cool_down(cool_thing); } --- from cool_lib use CoolThing; from cooler_lib use * as cooler; from ~/repo_relative/dir/a_file use SomeType::ATraitImpl; from relative/sub_dir/some_file use SomeType, some_func, SomeEnum::SomeVariant; }
Mutation semantics
#![allow(unused)] fn main() { let immutable_var = (1, 2, 3); // immutable_var = (2, 3, 4); // compile time error // immutable_var.0 = immutable_var.0 + 1; // compile time error let mut mutable_var = (1, 2, 3); mutable_var = (2, 3, 4); // mutable_var.0 = mutable_var.0 + 1; // compile time error let mutable_val_in_immutable_var = (1, 2, 3).mut; // mutable_val_in_immutable_var = (2, 3, 4); // compile time error imutable_val_in_mmutable_var.0 = imutable_val_in_mmutable_var.0 + 1; mutable_var.0 = 2; mutable_var.1 = 3; mutable_var.2 = 4; mutable_var.inc(); print(mutable_var); // Prints "(3, 4, 5)" immutable_var.inc(); // Compile time error impl (i32, i32, i32) { fn inc(mut self) { self.0 = self.0 + 1 self.1 = self.1 + 1 self.2 = self.2 + 1 } } }
*
not any
Denim uses the equivalent of TypeScript's unknown
, but calls it *
instead.
#![allow(unused)] fn main() { let some_json = "{hello: \"world\"}"; let parsed_json: {str: *} = parse_json(some_json); let world = some_json["hello"]; world.lower.print(&mesage); // Compile time error because `world` is `*`. world.as(str).lower.print(&mesage); // ππΎ --- from convert use parse_json; }
Keyword suffixing
#![allow(unused)] fn main() { choice.match { Yes => print("yes!"), No => print("no.") }; result.try; predicate.if { print("true") } else { print("false") }; eventual.await; operation.async; let three_point_o = 12.as(f32) / 4; let ref_to_some_fn = some_fn.fn; [1, 2, 3].mut.add(4); }
Optional chaining
#![allow(unused)] fn main() { type Node<T> = { next: Node?, val: T, }; fn fifth_node<T>(node: Node<T>) -> T where T: str|i32 { node.next.next.next.next.or_else("fallback") // No need for `?.`-style chaining - it is built in } }
Selfification
#![allow(unused)] fn main() { fn add(a: i32, b: i32) -> i32 { a + b } print(add(40, 2)); print(40.add(&a, 2)); }
Getterification
Caveats:
.fn
for function references- Must return something
#![allow(unused)] fn main() { fn rand(min = 0.0, max = 1.0) -> f32 { let (actual_max, actual_min) = if max > min { (max, min) } else { (min, max) }; (actual_max - actual_min) * secure_rand.trunc + actual_min } print(rand(min: -2.0, max: 2.0)); print(rand(max: 23.0)) print(rand()); print(rand); fn print_hello() { print("hello") } print_hello; // compile time error --- from crypto use secure_rand; }
body
param
Inspired by Kotlin trailing lambdas.
#![allow(unused)] fn main() { fn do(when: Time = Time::now(), body: fn() -> void) { pause(until: when); body(); } do(when: Time::now(), body: || print("hello world")) do(Time::now()) { print("hello world") } do { print("hello world") } fn do_with_args(body: fn(args: *) -> void) { body("args") } do_with_args { print(it); } fn do_with_return(body: fn() -> u8) { print(body); } do_with_return { 8 } --- from clock use pause, Time; }
Operator aliasing for Option and Result
#![allow(unused)] fn main() { fn why_write(a: Option<T>) {} fn when_you_could_write(a: T?) {} fn why_do(a: Result<T>) {} fn when_you_could_do(a: T!) {} fn why_use_many_word(a: Result<Option<T>>) {} fn when_few_word_do_trick(a: T?!) {} }
Destructuring
#![allow(unused)] fn main() { let tuple = (1, 2, 3); let (a, b, c) = tuple; let (..a_and_b, still_c) = tuple; let (a, b, ..c_in_a_tuple) = tuple; let struct = { w: (1.1, 2.2), x: 3, y: "4", z: false }; let { w: (w1, w2), x, y, z} = struct; let {..everything_but_z, z} = struct; let list = [1, 2, 3]; let [first_from_list] = list; let [_, second_from_list] = list; let map = ["i": -1, "j": -2, "k": -3]; let ["i": i, ..everything_but_i] = map; enum E { Bools { m: bool, n: bool }, Nums(i32), Nothing, } let e1 = E::Bools { m: false, n: true }; let e2 = E::Nums(0.01); let E::Bools { m } = e1; let E::Nums(num) = e2; }
Autoboxing
#![allow(unused)] fn main() { let optional_num: i32? = 123; // `123` gets turned into `Some(123)` automatically fn box_me(n: i32) -> i32? { n } let str_result: str! = "hello"; // `"hello"` gets turned into `Ok("hello")` automatically fn box_me_again(s: str) -> str! { s } let optional_bool_result: bool?! = true; // `true` gets turned into `Ok(Some(true))` automatically fn box_me_baby_one_more_time(b: bool) -> bool!? { b } }
Lexical concurrency
#![allow(unused)] fn main() { fn waste_time() -> i32 { // t = 0s sleep(1.seconds); let a = 1; sleep(50.seconds); let b = 2; sleep(2.seconds); // t = 50s let eventual = sleep(1000.seconds).async; // t = 50s eventual.await; // t = 1050s a + b } }
Anonymous structs
#![allow(unused)] fn main() { type Foo = { foo: bool }; type Bar = { bar: i32 }; type FooBar = Foo | Bar; fn print_foobar(foobar: FooBar) { let { foo, bar } = foobar; foo.match { Some(foo) => print("foo: $foo"), None => print("bar: ${bar.unwrap}}"), } } }
A little of everything
- Common-sense builtins
- Dart-style doc comments
- Go-style packaging and visibility
- Rust-style enums
- Rust-style pattern matching
- Strict param labeling
- Imports at the bottom
-
*
notany
- Mutation semantics
- Keyword suffixing
- Optional chaining
- Selfification
- Getterification
-
body
param - Operator aliasing for Option and Result
- Destructuring
- Lexical concurrency
- Anonymous structs
- Minimum viable operators
- Function overloading
- Trait-based inheritance
-
impl
anything
#![allow(unused)] fn main() { enum Weather { Cloudy, Sunny { uv_index: f32 }, PartlyCloudy, Rainy(RainForecast), } /// Fetches information about the wetaher for the specified [date] in the /// location indicated by [zip]. pub fn fetch_weather_data(date: Date, zip: ZipCode) -> [WeatherData]! { let raw_weather_data = fetch("https://theweather.com/$zip?date=${date.as_dd_mm_yyyy}") .try .as_str; raw_weather_data .lines .map(|line| WeatherData::parse(line)) .collect(to_result_list.fn) .ctx("Parsing each line of raw weather data") } fn seven_day(zip: ZipCode) -> [WeatherData!] { let today = Date::today; let list_of_eventuals: [Eventual<WeatherData!>] = 0..7.iter() .map(|day_offset| today.prev_day(day_offset)) .map(|date| date.fetch_weather_data(&date, zip).async) .collect(to_list.fn) let eventual_of_list: Eventual<[WeatherData!]> = list_of_eventuals.flattened; eventual_of_list.await } impl DoI { fn need_an_umbrella_today(home_zip: ZipCode, work_zip: ZipCode) -> bool { // The two invocations of `fetch_weather_data` happen concurrently: let home_weather_data = fetch_weather_data(date: Date::today, zip: home_zip); let work_weather_data = fetch_weather_data(date: Date::today, zip: work_zip); home_weather_data.matches(Weather::Rainy) or work_weather_data.matches(Weather::Rainy) } } impl WeatherData { fn as_weather(self) -> Weather { self.match { WeatherData { rain_data } if rain_data.pct_chance_of_rain > .5 => Weather::Rainy(RainForecast::from(rain_data)) WeatherData { sun_quotient, uv_index } if sun_quotient > .7 => Weather::Sunny { uv_index }, WeatherData { sun_quotient } if sun_quotient <= .7 and sun_quotient > .25 => Weather::PartlyCloudy, WeatherData { sun_quotient } => Weather::Cloudy, } } } --- from location use ZipCode; from time use Date; from ~/my/weather/lib use WeatherData; from ./util/web use fetch; }
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:
- 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" }
- Functions can have a special parameter
block
of typefn() -> 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!
Tour
There is always where you want to start with a new language - what will Denim look like? The answer is a lot like Rust. I just happen to think that Rust gets a lot of stuff right. That said, expect some deviations made in the interest of developer ergonomics and Denim's particular domain challenges.
Primitives
Denim's primitives are mostly stolen from Go. It has:
-
bool
Boolean value that can betrue
orfalse
.A
bool
literal is eithertrue
orfalse
. -
byte
An 8-bit unsigned integer, often in strings. NOTE: in the context of astring
,byte
does not encapsulate the semantic concept of a "character" since in some encodings, like UTF-8, a character may be expressed with more than one byte.A
byte
literal is just a non-negative number like128
. -
float
A 64-bit signed floating point number.A
float
literal is either dot-delimited decimal numbers like-12.80001
, or scientific notation like1.2e-12
. -
int
A 64-bit signed integer number.An
int
literal is just a number like11
or-7
. Notably, Denim does not support binary, hex, or octalint
literals. -
rune
A 32-bit unsigned integer number intended to represent the semantic concept of a "character" in strings.A
rune
literals is just a single-quoted character like'k'
or'π©'
. -
string
A sequence of bytes semantically associated with text.A
string
literal is usually a quoted span of text like"hello world"
, but it comes in other flavors too."\"With escaped characters\t"; """A multiline string"""; r#" a raw string where \n escaped characters are not a thing "#;
Denim also supports Dart-style
string
concatentation. You can concatentate string literals by declaring them adjacent to one another. Though many other languages support it however, Denim will not supportstring
concatentation via the+
operator."this a long string that I'm' worried will end up exceeding the line limit, " "but luckily I can just continue with another adjacent string on the " "next line";
string
interpolation will look very familar to fans of ES2015+ or Dart."${1 + 2 + 3 + 4 + 5} is fifteen"; "You can also reference $some_variable without the `{}`";
Special primitives
It is important to note that Denim does not have a null-type like Go's
nil
. The closest idea that Denim has in this regard is the void
. The
void
type has exactly one value, void
, and is used when there is no other
meaningful value that could be returned. void
, much like Rust's ()
, is most
commonly seen implicitly: functions without a ->
implicitly have a void
return type.
#![allow(unused)] fn main() { // The functions below are equivalent: fn long() -> void {} fn short() {} }
Denim also steals the unknown
type from TypeScript. unknown
represents all
possible values. Every type is assignable to type unknown
. Therefore the type
unknown
is a universal supertype of the type system. However, the Denim
compiler won't allow any operation on values typed unknown
- the values must
first be cast to a narrower type. For more on this concept, check out
some information on TypeScript's unknown
.
Variables
Denim steals variable declaration from Rust.
#![allow(unused)] fn main() { let abc = 123; }
Like in Rust, Denim's let
creates immutable variables by default. This means
that abc
cannot by be assigned a new value.
#![allow(unused)] fn main() { let abc = 123; abc = 321; // Compile-time error }
To create a mutable variable, you need to add a mut
prefix to the let
keyword.
#![allow(unused)] fn main() { let mut xyz = 123; xyz = 456; // π }
Importantly, Denim does not have a notion of const
. Instead let
is also used
to declare constants at the top-level lexical scope,
Printing
Denim ships with one main way to print out to the console - print
.
print
is a function (explained in greater depth later) that takes a string
which it prints to its own line in the console. In the browser, this means it
calls console.log(...)
under the hood. Outside of the browser, it appends a
line to stdout.
#![allow(unused)] fn main() { print("hello world"); // Prints "hello world" on its own line. }
It is worth noting that more sophisticated logging facilities are available via library.
Tuples
Tuples are a fixed-size collection of different types. They can be helpful in situations where you want to group a few different pieces of data without creating a dedicated, named data structure for them. Rust Tuples are great. Why mess with a good thing? Denim Tuples are functionally identical.
#![allow(unused)] fn main() { // Tuple of an `int`, a `string`, and a `bool`. By the way, `let` is how we // create variables in Denim. We'll elaborate in depth a little later. let tuple = (123, "456", true); // You can read different parts of a Tuple with dot notation. print(tuple.0); // Prints "123" print(tuple.1); // Prints "456" print(tuple.2); // Prints "true" print(tuple.7); // Compile-time error }
While Denim Tuples are always immutable, they can be quite ergonomic to use and
manipulate. Tuples can be composed together via the ...
operator, and split
apart in a similar way through de-structuring.
#![allow(unused)] fn main() { let x = (1.2e-3, 'e', false); let y = ("yo", ...x, x.1); print(y); // Prints "(yo, 0.0012, e, false, e)" let (first, ...middle_stuff, last) = y; print(first); // Prints "yo" print(third); // Prints "e" // NOTE: `middle_stuff` is itself a Tuple. print(middle_stuff); // Prints "(0.0012, e, false)" }
Array
Perhaps the most common collection in most languages is an array - an ordered
sequence of values that supports random access. In JavaScript, it is called
Array
while in Rust it is called Vec
. Denim Arrays should look feel and
behave like Dart's List
or JavaScript's Array
.
#![allow(unused)] fn main() { let array = [1, 2, 3]; }
Like in other languages, Denim Arrays support random access by index with the
[]
operator.
#![allow(unused)] fn main() { // The type of `array` is inferred to be `[int]` here. let array = [1, 2, 3]; print(array[0]); // Prints "1" print(array[2]); // Prints "3" print(array[17]); // Compile-time error }
Need your Array to be mutable? Prefix the literal with a mut
.
#![allow(unused)] fn main() { let mutable_array = mut [1, 2, 3]; }
Sometimes when you have a mutable Array, it starts empty. In this situation, the
inner type of the Array is ambiguous, so it falls back to unknown
by default.
You can help provide more type information on the variable or explicitly cast
the Array literal to correct this.
#![allow(unused)] fn main() { let another_array: mut [string] = mut []; // In Denim, like in Rust, you can cast a value with the `as` keyword. let yet_another_array = [] as [bool]; }
Denim can also infer the inner type of the Array later on from context.
#![allow(unused)] fn main() { // `one_more_for_the_road` is starts as a `mut [unknown]`. let one_more_for_the_road: = mut []; // Now Denim knows that `one_more_for_the_road` must be an `mut [int]`. one_more_for_the_road.add(2) }
Denim Arrays have lots of helpful methods focused on mutation.
#![allow(unused)] fn main() { let some_array: mut [int] = mut []; some_array.add(2); some_array.add(1); print(some_array); // Prints "[2, 1]" some_array.remove_at(0); print(some_array); // Prints "[1]" print(array[2]); // Prints "3" print(mutable_array[0]); // Prints "2" }
Denim Arrays ship with special syntax to instantiate arrays with a fixed number of identical values.
#![allow(unused)] fn main() { let eight_zeroes = [0; 8]; // `eight_zeroes` is an `[int]` let four_strings = [""; 4]; // `eight_zeroes` is an `[string]` }
Denim Arrays are spreadable with ...
just like JavaScript arrays.
#![allow(unused)] fn main() { let x = [1, 2, 3]; let y = [...x, 4, 5, ...x, 6]; print(y); // Prints "[1, 2, 3, 4, 5, 1, 2, 3, 6]" }
Denim allows for ergonomic slicing and dicing of arrays via de-structuring.
Denim Array de-structuring is very similar to JavaScript Array
de-structuring.
#![allow(unused)] fn main() { let y = [1, 2, 3, 4, 5, 6]; let [first, second, ...middle_stuff, last] = y; print(first); // Prints "1" print(second); // Prints "2" print(last); // Prints "6" // NOTE: `middle_stuff` is itself an Array. print(middle_stuff); // Prints "[3, 4, 5]" }
Enums
Enums are a great way to model data that is best described in categories. For
example, the concept of "days of the week" could be accurately described a
string
, but since there are only seven kinds of them, enum
is a better fit.
enum
allows you to explicitly enumerate each variant.
#![allow(unused)] fn main() { enum DayOfTheWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, } }
Broadly speaking, explicit enumeration via enum
makes validation and pattern
matching over your data more ergnomic and less error prone. Sometimes, it makes
sense to also attach data to each variant, allowing enum
to function more like
structs or classes in other languages.
#![allow(unused)] fn main() { enum IpAddress { // V4 IP addresses look like "192.168.1.1". // // Each `IpAddress::V4` has an inner field of type `[int]`. V4([int]), // V6 IP addresses look like "2001:db8::2:1". // // Each `IpAddress::V6` has a `segments` field of type `string` and a // `segment_count` of type `int`. V6 { segments: string, segment_count: int }, } let some_v4_address = IpAddress::V4([192, 168, 1, 1]); print(some_v4_address.0); // Prints "[192, 168, 1, 1]" // `segments` does not need to be explicitly specified since it is the only // field of `IpAddress::V4`. let some_v4_address = IpAddress::V6 { segments: "2001:db8::2:1", segment_count: 5, }; print(some_v6_address.segments); // Prints "2001:db8::2:1" print(some_v6_address.segment_count); // Prints "5" }
Special enums
TODO(skeswa): flesh this out by copying (Option
, Result
) from rust.
#![allow(unused)] fn main() { pub enum Option<T> { None, Some(value: T), } pub enum Result<T, E> where E = Error { Err(error: E), Ok(value: T), } }
We have special syntactic sugar for Option
:
#![allow(unused)] fn main() { // This: let x: Option<string>; // Is the same as this: let x: string?; }
Option
is used in much of the same way that null
, nil
, or undefined
is
used in other languages - to communicate that a value may be absent.
Unfortunately, dealing with Option
and other similar ideas is sort of awkward.
You end up writing a lot of if-this-then-that logic dealing with whether the
value is there or not.
if (!thing.x || !thing.x.y || !thing.x.y.z) {
return "nope";
}
return thing.x.y.z.value;
To reduce the cruft, lanuages like JavaScript added the
?.
and
??
operators.
return thing.x?.y?.z?.value ?? "nope";
Better right? Well, sure. Except that now you have two operators that basically
do the same thing depending on context - .
and ?.
. They enable access into
the state and behavior of values and objects.
In Denim, all instances of .
behave like ?.
does in other languages - it
falls back to Option::None
automatically when an optional value is absent.
Denim retains ??
as syntactic sugar for Option::unwrap_or(..)
:
#![allow(unused)] fn main() { thing.x.y.z.value ?? "nope"; }
Maps
TODO(skeswa): flesh this out (Dart Maps).
#![allow(unused)] fn main() { // Below, `x` is inferred to be of type `{string: int}`. let x = { "one": 1, "two": 2, "three": 3, }; let y: {string: unknown} = { 1: "one", 2: [1, 2], 3: {"hello": "world"}, }; print(x["one"]); // Prints "Some(1)" print(y[2]); // Prints "Some([1, 2])" print(x["four"]); // Prints "None" print(y[7]); // Prints "None" }
Denim Maps, like other Denim data structures, are made mutable with a mut
prefix.
#![allow(unused)] fn main() { let mutable_map = mut {"one": 1, "two": 2}; }
Mutable Denim Maps feature useful methods and operators stolen from the Maps of other languages.
#![allow(unused)] fn main() { let mutable_map: mut {string: string} = mut {}; mutable_map["hello"] = "world"; mutable_map["foo"] = "bar"; print(mutable_map); // Prints "{hello: world, foo: bar}" mutable_map.remove_key("hello"); print(mutable_map); // Prints "{foo: bar}" }
TODO(skeswa): spread notation
#![allow(unused)] fn main() { let x = { "one": 1, "two": 2, "three": 3, }; let y = {...x, "four": 4}; }
TODO(skeswa): destructuring
#![allow(unused)] fn main() { let {"one": one, "two": two, ...everything_else} = y; }
Sets
TODO(skeswa): flesh this out (Dart Sets).
#![allow(unused)] fn main() { // The type of `x` is inferred to be `{string}` here. let x = {"one", "two", "three"}; let y = mut {1, 2.2, 3}; }
TODO(skeswa): spread notation
#![allow(unused)] fn main() { let x = { "one": 1, "two": 2, "three": 3, }; let y = {...x, "four": 4}; }
TODO(skeswa): no destructuring
Type Aliases
Denim allows you to come up with another name for an existing type using
something called a type alias. Like Rust type aliases, Denim type aliases are
declared with the keyword type
. For example, the following defines the type
Point
as a synonym for the type (int, int)
:
#![allow(unused)] fn main() { type Point = (int, int); let p: Point = (41, 68); }
Type aliases can be useful to abbreviate long, verbose types. Type aliases also come in handy when attaching more semantic meaning or description to a common type, like a tuple in the case above, would make your code easier to reason about.
Operators
and
,or
Denim steals these logical comparison operators from Python. Why? Well, truth be told, it is mostly to reduce the ambiguity of||
(see: Rust closure syntax). But also, I think it sorta reads nicely since keywords are highlighted to be pretty eyecatching usually.==
,!=
The strict equality and inequality operators work just the way that you think they do: they check if primitives are equal, or if non-primitives point to the same address in memory.===
,!==
These two operators are congruence and incongruence operators in Denim. They are meant to check if two values are qualitatively equal or not. We use theEq
trait to implement these operators.+
,-
,*
,/
, and%
Arithmetic operators can only be applied to numbers of the same kind.**
, the exponentiation operator, is stolen from Python~/
, the truncating division operator, is stolen from Dart
You might be wondering where the bitwise operators are - there are none! Looking for operator overloads? You won't find them here.
Good riddance.
Comments
Comments are almost purely stolen from Rust.
#![allow(unused)] fn main() { // This is a simple line comment. let abc /* this is an inline comment */ = /* stick these anywhere */ 123; /// This is a doc comment. /// /// These comments are intended to document your code in greater detail. To /// facilitate this greater detail, these kinds of comments have: /// 1. **full** _Markdown_ `support` /// 2. Dart-style `[]` code links /// For example, [abc] references the variable created above explicitly. let forty_two = 42; }
Expressions and statements
Following Rust's lead, Denim is (mostly) an expression language. Rust's documentation does a good job describing what this means, and some of its implications:
Most forms of value-producing or effect-causing evaluation are directed by the uniform syntax category of expressions. Each kind of expression can typically nest within each other kind of expression, and rules for evaluation of expressions involve specifying both the value produced by the expression and the order in which its sub-expressions are themselves evaluated.
In contrast, statements in Rust serve mostly to contain and explicitly sequence expression evaluation.
The quoted description can be a bit difficult to fully understand, but it
basically boils down to a simple mantra: in Denim, almost everything, including
control flow like if...else
, is an "expression" can be used like a value.
Expressions can be terminated, and their values contained, by capping them with
a ;
character. Loosely, a terminated expression is a "statement".
Perhaps the best way to visualize this is to demonstrate an example involving
if...else
, Denim's simplest branching control flow expression.
#![allow(unused)] fn main() { // Pretend that `some_random_int` is defined elsewhere and is a randomly // generated `int`. let x = some_random_int; // Below, `message`'s value results from an `if...else if...else` expression // on `x`. When `x` is `4`, `message` is `"x is four"`. Also, if `x` is not `3` // or `4`, `message` is the empty string. let message = if x == 4 { "x is four" } else if x == 3 { "x is three" } else { "" }; // As you can see, expressions can also be used in a sort of stand-alone // fashion. if !message.is_empty() { print(message); } else { print("Looks like there is nothing to say!"); } }
Keywords
- Stolen from Rust
as
async
- Suffixable
await
- Suffixable
break
continue
else
enum
extern
false
fn
for
if
- Suffixable
impl
in
let
loop
match
- Suffixable
mod
pub
pub(pkg)
for directory-level visibilitypub(repo)
for repository-level visibility
return
self
Self
- Special type
struct
trait
true
type
use
where
while
- Suffixable
- Stolen from Dart
is
- Used for Dart-style type checking
show
try
- Suffixable
void
- Stolen from Python
from
- Stolen from TypeScript
unknown
- Originals
fork
- Suffixable
tandem
Suffixing
In Denim,some keywords can be applied as suffixes with .
notation. Namely,
async
, await
, fork
, if
, match
, try
, and while
.
#![allow(unused)] fn main() { let some_eventual_function_value = async some_function("abc"); let some_other_eventual_function_value = some_other_function("abc").async; print(await some_eventual_function_value); print(some_other_eventual_function_value.await); fork some_struct { some_field: "123", }; some_struct.fork { some_field: "123", }; if some_bool_expression { print("its bigger") } else { print("its _not_ bigger") } some_bool_expression.if { print("its bigger") } else { print("its _not_ bigger") } some_number.match { 1..=122 => print("nope"), 123 => print("yep"), _ => print("nope"), } match some_number { 1..=122 => print("nope"), 123 => print("yep"), _ => print("nope"), } fn x() -> Result { something_that_can_fail(123).try; try { let mut a = 123; a = a * 2; something_that_can_fail(123) } } let mut i = 0; while i < 3 { print("i is $i"); } let mut is_done = true; is_done.while { is_done = false; } }
Functions
Syntactically, Denim functions are very similar Rust functions.
#![allow(unused)] fn main() { // Functions can specify a return type using the `->` symbol. pub fn multiply_by_two(num: float) -> float { // Functions implicitly return the last value in their body. Since, the next // line is not terminated by a `;`, it evaluates to `num * 2`. num * 2 } // No need to specify if a function is `void`, just say nothing at all: fn print_hello_world() { // By the way, printing works the same way it does in Dart. `print` is a // globally visible function that takes a `string` and outputs it to // console/stdout. print("hello world"); } }
There is just one wrinkle with Denim functions - there are no positional arguments, only named arguments that can appear in any order. When a function is invoked, its arguments must be explicitly labeled at the call-site unless a variable is passed along sharing the name of an argument. There is one exception to this rule: if a function has just a single argument, no label is necessary.
#![allow(unused)] fn main() { fn multiply_by_two(num: float) -> float { num * 2 } print(multiply_by_two(3)); // prints "6" fn multiply(a: float, b: float) -> float { a * b } print(multiply(a: 2, b: 5)); // prints "10" let b = 5; print(multiply(a: 2, b)); // prints "10" }
There are situations where you need multiple arguments, but naming them would be a little silly. The best way to do this in Denim is to have a tuple as your only argument.
#![allow(unused)] fn main() { fn add(nums: (int, int)) -> int { nums.0 + nums.1 } }
Denim functions can also have optional arguments. One way to accomplish this is to specify a default value for a parameter.
#![allow(unused)] fn main() { // Below Denim infers that `action` is a `string` from its default value. // // We explicitly specify a type for `times` because `= 1` would it an `int` by // default. // // NOTE: default values must be immutable. fn i_cant_wait_to(action = "take a nap", times: float = 1) { print("Time to $action $times time(s)!"); } print( i_cant_wait_to( action: "eat donuts", ), // prints "Time to eat donuts 1 time(s)!" ); print( i_cant_wait_to( action: "eat donuts", times: 1.5, ), // prints "Time to eat donuts 1.5 times(s)!" ); print(i_cant_wait_to()); // prints "Time to take a nap 1 times!" }
Another way to declare an optional argument is to make its type T?
. T?
represents an optional value of type T
. We'll describe T?
in greater depth
later, but it works identically to how Rust's
Option<T>
works.
#![allow(unused)] fn main() { fn measurement(scalar: float, unit: string?) { let lowercase_unit = unit.to_lower() ?? ""; "$scalar $lowercase_unit" } print(measurement(scalar: 12.5, unit: Some("px"))); // prints "12.5px" print(measurement(scalar: 12.5, unit: None)); // prints "12.5" }
Denim also includes some syntactic sugar to make using this a little less
verbose by allowing Some
or None
to be implied by the respective inclusion
or exclusion of an argument.
#![allow(unused)] fn main() { print(measurement(scalar: 12.5)); // prints "12.5" print(measurement(scalar: 12.5, unit: "px")); // prints "12.5px" }
Denim also has a convenient alternate syntax for anonymous functions, called lamba functions, that it borrows from Rust closures. Unlike Rust closures however, Denim lambda functions are just plain ol' functions with no extra or special behaviors.
#![allow(unused)] fn main() { fn sum_three(a: int, b: int, c: int) -> int { a + b + c; } let sum_three_as_a_lambda = |a: int, b: int, c: int| a + b + c; }
The argument types of a lambda can be inferred if enough information is provided at the call site.
#![allow(unused)] fn main() { let inferred: fn(a: int) -> int = |a| a + 1; }
By the way, function types look like this:
#![allow(unused)] fn main() { fn(a: A, b: B, c: C) -> D; }
Denim has a special syntax for functions that receive an inlined anonymous
function as an argument. This common when passing callbacks and in embedded
DSLs. Any function argument named body
can have an inline block after its
invocation that works like a lambda function.
#![allow(unused)] fn main() { fn element(name: string, body: Option<fn() -> Element>) -> Element { Element { name, child: body() } } // This: element(name: "div", body: || { element(name: "button", body: || { span("click me") }) }) // Is the same as this: element("div") { element("button") { span("click me") } } }
Thanks to "getterification", we can make this syntax even sweeter:
#![allow(unused)] fn main() { fn button(body: Option<fn() -> Element>) -> Element { element("button", body) } fn div(body: Option<fn() -> Element>) -> Element { element("div", body) } div { button { span("click me") } } }
TODO(skeswa): it
for the singular argument passed to body
.
Modules
Denim's core packaging primitive is called a "module". Modules designed to define behavior, state, and types for a particular context or domain. Furthermore, Denim modules are intended to depend on each other through the import and export of behavior, state, and types.
Denim modules are expressed a file system as a directory with Denim source files. Each source file in an Denim module has full visibility of everything declared in the other source files of the module. Additionally, each source file can do its own importing and exporting. Denim source files can only import stuff exported by another Denim module. It might help to think of this style of packaging is a blend of Go packages and ES modules.
Like Deno and Go, remote modules are fetched from source control with an accompanying version. Version is typically inferred from a tag or reference on the imported module repository.
#![allow(unused)] fn main() { from "github.com/abc/xyz@v0.9.0" use { hello, world }; }
Denim allows imports from other modules in the same repository, too. These are
called respository-relative imports. ~
always refers to the root of the
repository containing the current module. Respository-relative imports do not
specify a version because the version will always be the same as the current
module.
#![allow(unused)] fn main() { from "~/bing/bang" use { boom: bΓΌm }; // Aliases `boom` to `bΓΌm`. from "~/some/sub/module" use { something }; // You can use ... syntax to import "everything else". from "github.com/foo/bar@v4.2" use { Foo, bar, ...foo_bar }; from "github.com/bing/bong@latest" use { ...bing_bong }; }
You can also re-export stuff using the show
keyword.
#![allow(unused)] fn main() { from "github.com/abc/xyz@v0.9.0" show { hello }; from "~/bing/bang" show { boom: bΓΌm }; }
Like in Rust, you can export stuff from Denim modules with the pub
keyword.
Anything not declared with a pub
will only be visible to things in its own
module.
#![allow(unused)] fn main() { pub let stuff_outside_of_this_module_can_see_me = true; }
Structs
You may now be wondering how more complex data structures are created and managed in Denim. I'm sure you are so shocked to find out that we (mostly) stole Rust syntax here too.
#![allow(unused)] fn main() { // We can make some or even all of the `struct` or its fields visible to // external modules using the `pub` keyword. pub struct User { active = false, /// You can put doc comments directly on `struct` fields. coolness_rating: int, pub name: string, pub nickname: string?, } }
We can instantiate and use struct
like this:
#![allow(unused)] fn main() { let some_user = User { active: true, coolness_rating: 42, name: "Some User", nickname: None, }; }
In the User
struct
above, two fields are optional - active
and nickname
.
active
is made optional by the specification of a default value for it,
false
. nickname
is optional because it is of type Option<string>
. Optional
fields may be excluded when a struct is instantiated.
#![allow(unused)] fn main() { let sam = User { coolness_rating: 11, name: "Sam", // Since `nickname` is an `Option<string>` we can simply specify a `string`. nickname: "S-money", }; let tria = User { active: true, coolness_rating: 12, name: "Tria", }; print(tria.nickname); // Prints "None". let jen = User { active: true, coolness_rating: 13, name: "Jen", // Below, `"Jenners"` is automatically wrapped in a `Some(..)`. nickname: "Jenners", }; }
Denim sugarifies Rust's default impl
by simply allowing you to declare methods
within the struct
itself. This should feel familiar to anyone coming from a
language that makes heavy use of classes.
#![allow(unused)] fn main() { struct Car { make: string, model: string, owner: User, // `self` is a special argument. Like in Rust, it means that this method is // attached to any instance of `Car`. fn drive(self) { print( "A ${self.model} ${self.model} owned " "by ${self.owner.name} is driving", ); } // Functions defined within a `struct` don't have to be attached to a // particular instance of that struct. They can instead function like static // methods in other languages. For instance, you would invoke this function // with `Car::create_lemon_for(some_user)`. fn create_lemon_for(owner: User) { Car { make: "AMC", model: "Pacer", owner } } } let some_user = User { active: true, coolness_rating: 42, name: "Some User", }; let my_car = Car { make: "Mazda", model: "Miata", owner: some_user, }; my_car.drive(); // Prints "A Mazda Miata owned by Some User is driving" }
As structs a big part of the language, Denim has some syntactic sugar to make instantiating nested structs ergonomic.
#![allow(unused)] fn main() { let my_other_car = Car { make: "Nissan", model: "Altima", owner: { active: false, coolness_rating: -1, name: "Another User", }, }; my_car.drive(); // Prints "A Nissan Altima owned by Another User is driving" }
One important thing to note here is that Denim structs, like most Denim data structures, are immutable by default. So, direct imperative mutation of Denim structs won't work in all the cases that you may be used to.
#![allow(unused)] fn main() { let my_car = Car { make: "Mazda", model: "Miata", owner: some_user, }; my_car.make = "Toyota"; // Compile-time error. }
The only way to create a mutable struct
instance is to create it with a mut
prefixing the struct
type. In Denim, structs and traits with a mut
are
internally mutable.
#![allow(unused)] fn main() { let my_mut_car = mut Car { make: "Mazda", model: "Miata", owner: some_user, }; my_mut_car.make = "Toyota"; // π }
All Denim structs can be shallow cloned with fork
. This useful when a field
inside an immutable struct
value should change.
#![allow(unused)] fn main() { let my_car = Car { make: "Mazda", model: "Miata", owner: some_user, }; let my_other_car = my_car.fork { // `make` is changed, but all of the other fields stay the same. make: "Toyota", }; print(my_car.make) // Prints "Mazda" print(my_other_car.make) // Prints "Toyota" my_other_car.make = "Toyota"; // Compile-time error (`my_other_car` is not a `mut Car`) }
But what if you need the forked struct
instance to be internally mutable? This
is made possible by prefixing the fork
expression with a mut
.
#![allow(unused)] fn main() { let my_car = Car { make: "Mazda", model: "Miata", owner: some_user, }; let my_other_car = mut my_car.fork; my_car.make = "Toyota"; // π }
Sometimes you need to fork
a struct
instance nested within another struct
instance. Given how frequently this is necessary, it felt like a good idea for
Denim to ship with a dedicated syntax.
#![allow(unused)] fn main() { let my_car = Car { make: "Mazda", model: "Miata", owner: some_user, }; let my_other_car = mut my_other_car.fork { make: "Rivian", model: "R1T", user: fork { // `self` refers to the original value of this `User` field. coolness_rating: self.coolness_rating + 1, }, }; }
In some situations, you may want nested internal mutation: you way want to be
able to directly mutate an inner struct
instance nested within another
struct
instance. Denim supports this by declaring the inner struct
field as
mutable with mut
. Note that this nested internal mutability is only accessible
in situations where the surrounding type is itself mutable.
#![allow(unused)] fn main() { struct House { address: Address, owner: mut User, } struct Address { number: string, street: string, } let a_house = House { address: { number: "42", street: "Wallaby Way", }, owner: { coolness_rating: -1, name: "P. Sherman", }, }; a_house.owner.active = true; // Compile-time error (`a_house` is not a `mut House`) let another_house = mut House { address: { number: "221B", street: "Baker St", }, owner: { coolness_rating: 99, name: "S. Holmes", }, }; a_house.owner.active = true; // π a_house.address.street = "Baker Street"; // Compile-time error (`House::address` is not an `mut Address`) }
Control Flow
TODO(skeswa): document branching TODO(skeswa): document loops TODO(skeswa): document loop labels (https://doc.rust-lang.org/rust-by-example/flow_control/loop/nested.html)
"Selfification" (a.k.a Functions as Methods)
Here, we take:
- "function" to mean a subroutine without a
self
reference to some piece of state - "method" to mean a subroutine with a
self
reference to some piece of state
#![allow(unused)] fn main() { fn authenticate(environment: Environment, auth_user: User) -> bool { // ... } // `&` is the secret sauce here fn do_stuff(environment: Environment, user: User?) { user.authenticate(environment, &auth_user).if { print("yes!"); } else { print("no!"); }; } }
"Getterification" (a.k.a Functions as Fields)
In Denim, functions can be treated as fields if they are not passed any arguments.
#![allow(unused)] fn main() { struct Foo { bar: int, pub fn baz(self, extra = 0) -> int { self.bar + extra + 1 } } let foo = Foo { bar: 27 }; print(foo.baz(2)); // prints "30" print(foo.baz()); // prints "28" print(foo.baz); // prints "28" }
Autoboxing
Given the prevalance and importance of special enums Option<T>
and Result<T>
in Denim, you might find yourself wrapping things in Some(..)
and Ok(..)
alot. To sweeten this syntax a little bit, Denim will automatically infer a T
as Some(T)
or a Ok(T)
depending on context; this is called "autoboxing".
#![allow(unused)] fn main() { // Why do this: fn foo() -> string? { Some("bar") } // when you could simply do this: fn autoboxed_foo() -> string? { "bar" } // `Result` is also autoboxable: let mut x: Result<int> = Ok(1); x = 2; // Autoboxing works in structs too! struct X(Option<int>); let x = X(1234); }
Traits
TODO(skeswa): flesh this out.
#![allow(unused)] fn main() { pub trait Summary { fn summarize(self) -> string; } pub trait Tweak<T> { tweakable: T?; fn tweak(mut self, some_other_arg: string) -> Self; } pub trait DefaultImpl { fn foo(mut self) -> string { "bar" } } pub trait MethodGeneric { fn some_method<T>(self) -> T; } }
Impls
Declaration:
#![allow(unused)] fn main() { // non-public impl B for A { fn b(self) { print("b"); } } // non-public impl A for X { fn a(self) { print("a"); } } impl Y for X { fn y(self) { print("y"); } } impl Z for X { fn z(self) { print("z"); } } // non-public impl X { fn new_functionality_for_x(self) { print("that newnew"); } } // non-public pub impl X { fn eat(self) { print("omnomnom"); } } }
Importing:
#![allow(unused)] fn main() { from "~/foo/bar" use { B for A, Y + Z for X, X, A, X::new_functionality_for_x, }; }
Generics
TODO(skeswa): flesh this out.
TODO(skeswa): where
.
Type Unions and Intersections
TODO(skeswa): flesh this out (Rust Type Unions (trait + trait) and TS Intersections (type | type).
#![allow(unused)] fn main() { type IntOrString = int | string; fn do_a_thing(data: IntOrString) { if data is int { print("it is an int") return; } print("it is a string") } fn do_another_thing(data: int | string | byte) { match data { is int => print("it is an int"), is! string => print("it isn't an string"), _ => print("it is probably a string"), } } trait Foo { fn bar(self) -> int, fn baz(self) -> bool, } trait Ping { fn bar(self) -> int, fn pong(self), } fn do_one_more_thing(data: Foo | Ping) { print(data.bar()) } type FooAndPing = Foo & Ping; fn do_one_last_thing(data: FooAndPing) { data.pong(); print(data.bar()) } }
Error handling
You can hoist the error out of returned Result
within a function that returns
Result
with the try
keyword.
TODO(skeswa): flesh this out.
#![allow(unused)] fn main() { fn do_a_thing() -> Result<void> { let csv_file_path = "a/b/c.csv"; let csv_data = read_file(csv_file_path).try.to_utf8(); basic_dance_move().context("tried to bust a move").try; } }
TODO(skeswa): flesh this out.
#![allow(unused)] fn main() { pub struct Error<ErrorKind> { pub cause: Error?; pub kind: ErrorKind; } }
TODO(skeswa): flesh this out.
#![allow(unused)] fn main() { panic("Oh no! We cannot recover from this!"); }
Pattern matching
TODO(skeswa): flesh this out.
Concurrency
In Denim, function invocations work differently than in other languages - they do not "block" by default. Instead, Denim functions work like co-routines, suspending themselves while invoking other functions, and resuming when they return.
#![allow(unused)] fn main() { // This call to `fetch` blocks, meaning that "done" will not be printed until // the fetch is finished. let weather = fetch(url: "https://get.theweather.com"); print("done"); // The code below: // 1. Prints "a" // 2. Waits for the first thing to be fetched // 3. Prints "b" // 4. Waits for the second thing to be fetched // 5. Prints "c" print("a"); let first_thing = fetch(url: "https://first.thing.com"); print("b"); let second_thing = fetch(url: "https://second.thing.com"); print("c"); }
To call a function concurrently, use the async
keyword. Using async
on a
function invocation wraps its return value in a special type called Eventual
,
like Future
or Promise
in other languages, is a concurrency monad that
allows you to subscribe to the completion of an asynchronous operation.
#![allow(unused)] fn main() { // This logic prints "a" then "b" then "c", printing the time in joburg and in // nyc (in no particular order) when the fetches complete later on. print("a"); let time_in_joburg = fetch(url: "https://worldtime.com/in/joburg").async; print("b"); let time_in_nyc = fetch(url: "https://worldtime.com/in/nyc").async; print("c"); time_in_joburg.then(|time| print("time in joburg: $time")); time_in_nyc.then(|time| print("time in nyc: $time_in_nyc")); }
Denim provides a way to "wait" for the Eventual
values returned by async
functions - the await
keyword. With await
, working with asynchronous
operations can be quite ergonomic.
#![allow(unused)] fn main() { // This logic: // 1. Starts fetching the time in atlanta // 2. Prints "a" // 3. Waits for the time in atlanta to be available before printing it // 4. Prints "b" let time_in_atlanta = fetch(url: "https://worldtime.com/in/atlanta").async; print("a"); print(time_in_atlanta.await); print("b"); }
There are a garden variety of helpful ways to wait on multiple asynchronous
operations at once in Denim. In particular, tuples are a great way to wait on
Eventual
values with different types.
#![allow(unused)] fn main() { // Any tuple of `Eventual` values has a method called `.after_all()` that waits // for every `Eventual` to complete. let (google_icon, _, c_text) = ( fetch(url: "https://google.com/favicon.ico").async, pause(Duration::new(milliseconds: 100)).async, readFile(path: "./a/b/c.txt").async, ).after_all().await; // Arrays of `Eventual` also support `.after_all()`. let icons = [ fetch(url: "https://nba.com/favicon.ico").async, fetch(url: "https://nhl.com/favicon.ico").async, fetch(url: "https://mlb.com/favicon.ico").async, ].after_all().await; }
Since this is quite a common pattern in Denim, there is a special syntax for it
enabled by the tandem
keyword. tandem { ... }
is simply syntactic sugar for
the concurrent invocation of multiple potentially asynchronous functions
normally enabled by the combined usage of .async
and .after_all().await
. As
such, a tandem { ... }
expression evaluates to a tuple of the return values of
each comma delimited sub-expression.
#![allow(unused)] fn main() { let (google_icon, _, c_text) = tandem { fetch(url: "https://google.com/favicon.ico"), pause(Duration::new(milliseconds: 100)), readFile(path: "./a/b/c.txt"), }; let icons = tandem { fetch(url: "https://nba.com/favicon.ico"), fetch(url: "https://nhl.com/favicon.ico"), fetch(url: "https://mlb.com/favicon.ico"), }; }
Both tuples and arrays of Eventual
support .race()
to wait for the first
Eventual
to complete, wrapping values in Option::None
to mark Eventual
values that lost the race.
#![allow(unused)] fn main() { let results = [ fetch(url: "https://later.com").async, fetch(url: "https://now.com").async, fetch(url: "https://way.later.com").async, ].race().await; print("$results"); // Prints "[None, Some(data), None]" }
Interop
TODO(skeswa): flesh this out.
#![allow(unused)] fn main() { trait Pluralizer { fn pluralize(self, text: string) -> string; } extern(go) { // Automatically compiles a Denim-friendly interface. from(go) "github.com/gertd/go-pluralize@0.2" use { ...pluralize }; struct GoPluralizer { client: pluralize.Client, } impl Pluralizer for GoPluralizer { fn pluralize(self, text: string) -> string { self.client.plural(text) } } pub fn new_pluralizer() -> impl Pluralizer { GoPluralizer { client: pluralize.NewClient() } } } extern(ts) { #[deno_types("npm:@types/pluralize@0.0.29")] from(ts) "npm:pluralize@8.0.0" use { pluralize }; struct TsPluralizer {} impl Pluralizer for TsPluralizer { fn pluralize(self, text: string) -> string { pluralize(text) } } pub fn new_pluralizer() -> impl Pluralizer { TsPluralizer {} } } }
Testing
TODO(skeswa): flesh this out.
In any "*.(spec|test).denim" file (spec = unit test, test = integeration test):
describe
, before
, test
are all functions that only apply to tests.
#![allow(unused)] fn main() { describe("Something") { before { } test("Something") { } #[timeout(seconds: 500)] test("Something") { } if !is_dev { test("Something") { } } } }