Crate typeables

Source
Expand description

§Typeables: Rust crate of type aliases and struct tuples

Typeables is a Rust crate of semantic types, such as unit types (e.g. metre for length, second for time), content types (e.g. email address, phone number), locale types (e.g. “en” for English, “zh” for Chinese), etc.

§Introduction

Typeables is based on the Rust pattern of “New Type”. This uses a Rust struct tuple as a wrapper of another type (or types), in order to provide encapsulation.

Example:

pub struct SecondAsF64(pub f64); // This is a "New Type" struct tuple.

let s = SecondAsF64(1.0); // One second of time as a floating-point 64-bit.

Typeables helps you write clearer code and stronger code, because you can be more-precise about your variable types and your function inputs and outputs.

Example:

use typeables::metre::MetreAsStructF64;
use typeables::second::SecondAsStructF64;

fn speed(
    m: MetreAsStructF64,
    s: SecondAsStructF64
) {
   println!("Speed is {}", m.0 / s.0)
}

fn main() {
    let m = MetreAsStructF64(2.0);
    let s = SecondAsStructF64(3.0);
    speed(m, s)
}

Typeables helps you create better domain driven design, stronger compile-time checking, and crisper run-time diagnostics.

§What is a struct tuple?

A struct tuple is akin to a wrapper for another type.

A struct tuple can make your code safer because it provides encapsulation.

Example:

pub struct Foo(pub f64);

let x = Foo(1.0);

§What is a type alias?

A type alias is akin to a nickname for another type.

A type alias can make your code clearer because it expresses your intent.

Example:

pub type Foo = f64;

let x: Foo = 1.0;

§How does Typeables provide a struct tuple and type alias?

Typeables provides concepts with a struct tuple and also with a type alias.

Example:

pub struct SecondAsStructF64(f64);

pub type SecondAsTypeF64 = f64;

§How do I refactor code to use Typeables?

Typeables helps you refactor from weaker-type code to stronger-type code.

Step 1. Start with typical code.

fn speed(
    m: f64,
    s: f64
) {
    println!("The speed is {}", m / s)
}

Step 2. Refactor to a Typeables type alias. This is annotation, because you clarify the types that the function expects.

fn speed(
    m: MetreAsTypeF64,
    s: SecondAsTypeF64
) {
    println!("The speed is {}", m / s)
}

Step 3. Refactor to a Typealias struct tuple. This is encapsulation, because you wrap the value in a struct.

fn speed(
    m: MetreAsStructF64,
    s: SecondAsStructF64
) {
    println!("The speed is {}", m.0 / s.0)
}

§Semantics

§Why use semantic names?

When you use semantic names, such as clear descriptions and purposeful naming conventions, then you help developers understand your code, and help compilers provide reliability, and help tools provide inspectability.

Suppose your code has this function:

fn f(year: i16, month: i16) {
    println!("Year {} Month {}", year, month)
}

A developer can use your code like this:

let year = 2022;
let month = 12;

f(year, month); // right
// f(month, year); // wrong, yet will compile and be a bug

You can make your code clearer by adding a type alias:

fn f(year: YearAsCommonEraAsTypeI16, month: MonthOfYearAsTypeI16) {
    println!("Year {} Month {}", year, month)
}

You can make your code stronger by using a struct tuple:

fn f(year: YearAsCommonEraAsStructI16, month: MonthOfYearAsStructI16) {
    println!("Year {} Month {}", year.0, month.0)
}

A developer can use your code like this:

let year = YearAsCommonEraAsStructI16(2022);
let month = MonthOfYearAsStructI16(12);

f(year, month); // right
// f(month, year); // wrong and won't compile

§Why use semantic names, representation names, unit names, and implementation names?

Suppose you’re writing an application for aircraft.

You want to keep track of:

  • Aircraft altitudes.

  • Representation as “Above Ground Level (AGL)” such as the height of the aircraft above the runway during takeoff or landing, or as “Mean Sea Level (MSL)” such as the worldwide height of the aircraft during cruising flight.

  • Unit of measurement as “Metre” which is the international system, or as “Foot” which is the United States system.

  • The implemention as a signed integer 16-bit, because altitude can be negative in some rare areas such as Death Valley California, and your application may need to integrate with legacy code that requires signed integer 16-bit numbers.

You can use this naming convention:

  • Semantic name “Altitude”

  • As representation name “Above Ground Level” or “Mean Sea Level”

  • As unit name “Metre” or “Foot”

  • As primitive name “I16”.

The code looks like this:

pub struct AltitudeAsAboveGroundLevelAsMetreAsStructI16(pub i16);
pub struct AltitudeAsAboveGroundLevelAsFootAsStructI16(pub i16);
pub struct AltitudeAsMeanSeaLevelAsMetreAsStructI16(pub i16);
pub struct AltitudeAsMeanSeaLevelAsFootAsStructI16(pub i16);

Suppose your app also needs to keep track of:

  • Airport elevations.

  • The representation as “Above Ground Level (AGL)” such as the height of an airport building above the airport runway, or as “Mean Sea Level (MSG)” such as the worldwide height of the airporse runway.

  • Etc.

The code looks like this:

pub struct ElevationAsAboveGroundLevelAsMetreAsStructI16(pub i16);
pub struct ElevationAsAboveGroundLevelAsFootAsStructI16(pub i16);
pub struct ElevationAsMeanSeaLevelAsMetreAsStructI16(pub i16);
pub struct ElevationAsMeanSeaLevelAsFootAsStructI16(pub i16);

The naming convention is crystal clear and fully descriptive:

  • Developers can understand your code better, and how to use it.

  • Compilers can provide stronger compile-time guarantees.

  • Debuggers can provide crisper run-time diagnostics.

  • Editors can provide better auto-complete and auto-suggest.

§Use words rather than abbreviations

Examples of semantic names:

  • Use “Latitude” not “Lat”.

  • Use “Longitude” not “Lon”, “Lng”, “Long”.

Examples of representation names:

  • Use “Decimal Degree” not “DD”

  • Use “Degree Minute Second” not “DMS”.

Examples of unit names:

  • Use “Metre” not “M”.

  • Use “Second” not “S”.

Examples of implementation names:

  • Use “TypeString” not “TS”

  • Use “StructString” not “SS”.

§Prefer singular over plural

Examples of representation names:

  • Use “Decimal Degree” not “Decimal Degrees”

  • Use “Degree Minute Second” not “Degrees Minutes Seconds”

Examples of unit names:

  • Use “Metre” not “Metres”.

  • Use “Second” not “Seconds”

§Prefer StemOfScope over ScopeStem

Example:

  • Use “DayOfWeek” not “WeekDay”

  • Use “DayOfMonth” not “MonthDay”

§Naming conventions

Naming convention for struct tuples:

pub struct FooAsStructI8(pub i8);
pub struct FooAsStructI16(pub i16);
pub struct FooAsStructI32(pub i32);
pub struct FooAsStructI64(pub i64);
pub struct FooAsStructI128(pub i128);
pub struct FooAsStructISize(pub isize);

pub struct FooAsStructU8(pub u8);
pub struct FooAsStructU16(pub u16);
pub struct FooAsStructU32(pub u32);
pub struct FooAsStructU64(pub u64);
pub struct FooAsStructU128(pub u128);
pub struct FooAsStructUSize(pub usize);

pub struct FooAsStructF32(pub f32); pub struct FooAsStructF64(pub f64);

pub struct FooAsStructStr(&'static String); pub struct FooAsStructString(pub
String);

Naming convention for type aliass:

pub type FooAsTypeI8 = i8;
pub type FooAsTypeI16 = i16;
pub type FooAsTypeI32 = i32;
pub type FooAsTypeI64 = i64;
pub type FooAsTypeI128 = i128;
pub type FooAsTypeISize = isize;

pub type FooAsTypeU8 = u8;
pub type FooAsTypeU16 = u16;
pub type FooAsTypeU32 = u32;
pub type FooAsTypeU64 = u64;
pub type FooAsTypeU128 = u128;
pub type FooAsTypeUSize = usize;

pub type FooAsTypeF32 = f32;
pub type FooAsTypeF64 = f64;

pub type FooAsTypeStr = str;
pub type FooAsTypeString = String;

§Comparisons

We recommend looking at the Rust crate uom (unit of measure) and the Rust book examples of the newtype pattern.

§Comparison with uom

Broadly speaking:

  • uom favors high-level work, such as automatic normalizations and conversions.

  • Typeables favors low-level work, such as exact representations and primitives.

Quantities v. units v. primitives:

  • uom deliberately favors working with conceptual quantities (length, mass, time, …) rather than measurement units (metre, gram, second, …) and implementation primitives (pub i8, u16, f32, …).

  • Typeables favors working with explicit measurement units and explicit implementation primitives. When you want the concept of “length” and unit “metre” and primitive “f32” then you write “LengthAsMetreAsTypeF32”.

Normalization v. exactness:

  • uom deliberately normalizes values to their base units, such as normalizing 1 gram to 0.001 kilogram, and deliberately trades away representation capabilities (due to inexact conversions) and precision capabilties (due to bit limits).

  • Typeables favors exactness, never normaliziation. When you want the concept of “mass” and unit “gram” and primitive “u128” for 128-bit unsigned integer precision, then you write “GramAsTypeI128”.

§Comparison with Rust “New Type Idiom” a.k.a. “New Type Pattern”

Broadly speaking:

  • The Rust “New Type Idiom” a.k.a. “New Type Pattern” is exactly what Typeables is doing with struct tuples. We like this idiom very much.

  • Typeables additionally provides type aliass. In practice we find this is an important way to help professional developers with larger codebases, because the developers can phase in the type aliass as hints to developers and to tools, then later on can phase in the struct tuples.

Roll your own versus using Typeables crate:

  • You can certainly roll your own new type pattern, and you can use your own type names, or even use the Typeables type names.

  • The Typeables crate is helpful because it provides a bunch of definitions, so you can use the crate, then get all the benefits of the types, plus your tools can use the crate information, such as for editor tool autocomplete and autosuggest.

§Implementation

The type aliases are all for Rust primitives and standards such as strings (using str and String) and numbers (using i64, u64, f64, et al.).

§Overhead

Typeables has zero or near-zero runtime overhead:

  • A type alias is zero runtime overhead because the type alias is replaced at compile time.

  • A struct tuple is near-zero runtime overhead because the struct tuple is a wrapper with a field.

§Typing

Typeables is deliberately verbose.

  • We use editors with autocomplete and autosuggest, so typing is easy and fast.

  • We like long names for low-level clarity.

  • Typeables defines many type aliass and struct tuples. Typically these are fast during development because they’re simple. Typically these are even faster during production because the Rust compiler can optimized these and also eliminate any that are not needed.

§Macros

The Typeables source code does not use macros.

  • We like macros in general.

  • Yet we discovered in practice that macros seem to interfere with some of our tooling.

  • For example, macros do not seem to work with some editors that inspect the Typeables crate in order to do autocomplete and autosuggest.

§Further reading

See: https://www.datafix.com.au/BASHing/2020-02-12.html

Modules§

adjective
Adjective
adverb
Adverb
altitude
Altitude
ampere
Ampere
becquerel
Becquerel
candela
Candela
currency
Currency
date
Date
date_time
DateTime
day
Day
day_of_month
DayOfMonth
day_of_week
DayOfWeek
day_of_year
DayOfYear
decimal_separator
Decimal Separator
degree_celcius
DegreeCelcius
dual_interval
Dual Interval
elevation
Elevation
email_address
Email Address
email_address_addr
Email Address Addr
email_address_name
Email Address Name
farad
Farad
global_location_number
Global Location Number
gram
Gram
gray
Gray
grouping_separator
Grouping Separator a.k.a. thousands separator for numbers
henry
Henry
hertz
Hertz
hour
Hour
hour_of_day
HourOfDay
html_text
HTML Text
international_standard_of_industrial_classification_revision_4_code
International Standard of Industrial Classification, Revision 4, Code
international_standard_of_industrial_classification_revision_4_name
International Standard of Industrial Classification, Revision 4, Name
joule
Joule
json_text
JSON Text
katal
Katal
kelvin
Kelvin
kilogram
Kilogram
latitude
Latitude
legal_entity_identifier_code
Legal Entity Identifier Code
litre
Litre
locale_code
Locale Code
locale_country_code
Locale Country Code
locale_language_code
Locale Language Code
locale_region_code
Locale Region Code
locale_script_code
Locale Script Code
locale_variant_code
Locale Variant Code
longitude
Longitude
lumen
Lumen
lux
Lux
markdown_text
Markdown Text
media_type
Media type
metre
Metre
metre_2
Metre^2
metre_3
Metre^3
metre_per_second
Metre Per Second
minute
Minute
minute_of_hour
MinuteOfHour
mole
Mole
month
Month
month_of_year
MonthOfYear
noun
Noun
ohm
Ohm
open_location_code
Open Location Code
pascal
Pascal
phone_e164_country_code
Phone E.164 Country Code
phone_e164_group_identification_code
Phone E.164 Group Identification Code
phone_e164_national_destination_code
Phone E.164 National Destination Code
phone_e164_subscriber_number
Phone E.164 Subscriber Number
phone_e164_text
Phone E.164 format
phone_e164_trial_identification_code
Phone E.164 Trial Identification Code
pronoun
Pronoun
quotation_start_delimiter
Quotation Start Delimiter
quotation_stop_delimiter
Quotation Stop Delimiter
radian
Radian
second
Second
second_of_minute
SecondOfMinute
siemens
Siemens
sievert
Sievert
steradian
Steradian
tesla
Tesla
time
Time
time_offset
Time offset as format “+HH:MM”
time_zone
Time zone as abbreviation text
unit_interval
Unit Interval
value_added_tax_identification_number
Value Added Tax Identification Number
verb
Verb
volt
Volt
watt
Watt
weber
Weber
week
Week
week_of_month
WeekOfMonth
week_of_year
WeekOfYear
what_free_words_code
What Free Words Code
xml_text
XML Text
yaml_text
YAML Text
year
Year
year_as_common_era
YearAsCommonEra