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; }