Summary of the Practical Type System (PTS) Article Series
First Published |
2023-10-17 |
Author |
Christian Neumanns |
Editor |
Tristano Ajmone |
License |
Introduction
This article provides a brief summary of the article series How to Design a Practical Type System to Maximize Reliability, Maintainability, and Productivity in Software Development Projects.
Note
This summary is updated each time a new article in the series has been published.
1. What, Why, and How?
Benefits of a practical type system:
-
increased expressiveness
-
more reliable and maintainable code
-
better support in IDEs and tools
-
more generic, reusable code
-
more efficient code
-
support for the First data, then code! approach
The cardinality of a type is the number of distinct values in its set of values. For example, type boolean
has a cardinality of 2, because there are two values: true
and false
.
To achieve its goal (reliability, maintainability and productivity), PTS is founded on two important rules:
-
The PTS Design Rule
A practical type system must fulfill two conditions:
-
The Easy! Condition
It is easy to:
-
Define new types with the lowest possible cardinality.
-
Quickly understand types by looking at their definition in the source code.
-
Change and maintain types.
-
-
The Fail Fast! Condition
Invalid values, type incompatibilities, and other type-related errors are detected immediately at compile-time, whenever possible.
If it's impossible to detect an error at compile-time then it must be detected as soon as possible at run-time.
-
-
The PTS Coding Rule
All data types in a software project should have the lowest possible cardinality.
2. Essence and Foundation
PTS is designed to maximize reliability, maintainability, and productivity in software development projects.
The Practical Type System (PTS) originated in the Practical Programming Language (PPL) — a compiled, high-level, object-oriented and functional programming language designed for application programming. PPL is a currently unmaintained prototype.
PTS is a set of ideas which I tried out and improved in PPL (a proof-of-concept implementation of PTS), found to be useful, and now want to share and discuss.
We need to differentiate between the PTS paradigm (set of features) and the PTS syntax. Both emerged from PPL, but they do not depend on each other. A programming language implementing the PTS paradigm could use a syntax which is very different from the PTS syntax shown in this article series.
PTS is not bound to a certain programming language paradigm. Most PTS concepts could be used, for example, in a procedural or functional programming language, or in a language designed for systems programming.
To maximize reliability and maintainability, a selected set of features is deliberately not supported in PTS. Some examples are:
-
Undefined state and undefined behavior
-
Silently ignored errors
-
Unsafe null-handling
-
Mutable data by default
-
Shared mutable data
-
Implicit type conversions/coercions
-
Truthy, falsy, and nullish values
-
Dynamic typing
-
Buffer overflows
-
Pointer arithmetics.
PTS provides the following core types:
-
Scalar types:
character
,string
,number
(integer
anddecimal
), andboolean
-
Collections:
list
,set
, andmap
-
null
(notMaybe
/Option
)
3. Record Types
PTS provides the following key features for record types (aka structure, struct, or compound data):
-
Concise and clear syntax — without noise or code duplication
-
Immutability by default
-
Built-in support for easy and flexible data validation, both for individual attributes and for the whole record
-
Default values for attributes
-
Named attribute assignments to improve readability, reliability, and maintainability
-
Type parameters to write generic, type-safe code
-
Type inheritance
-
A
with
operator to facilitate creating record copies with some values modified -
A
to_string
function for generating short, human-readable descriptions of record objects -
Structured documentation
-
Support for serializing/deserializing record objects.
These features aim to simplify and streamline working with record types, while also increasing reliability, maintainability, and safety.
In particular, the combination of easy and flexible data validation and immutability by default make PTS record types very robust.
4. Union Types
Without union types, an object reference (constant, variable, input parameter, etc.) is always an instance of a single type. For example, input parameter identifier
is declared to be of type string
.
Union types allow an object reference to be an instance of any type among a defined set of types. For example, input parameter identifier
is declared to be of type string or number or null
.
Union types provide the following benefits:
-
They are simple to understand, easy to use, and they provide an elegant solution for frequent programming tasks.
-
They provide a sound foundation for uniform null- and error-handling — two critical aspects of a practical type system.
Here's an example of a function that might return a
string
or afile_error
ornull
:fn read_text_file ( file file_path ) -> string or null or file_error // function body .
-
They help to simplify APIs, increase type-safety, facilitate eager/lazy evaluation, and provide additional benefits.
PTS provides a set of dedicated operators and statements to streamline working with union types.
5. Null-Safety
PTS uses null
to represent the absence of a value.
Union types are used to declare nullable object references (e.g. string or null
).
Null-safety is natively built into the type system, therefore null pointer errors cannot occur.
PTS provides a set of dedicated operators and statements to facilitate null-handling as much as possible.
Flow typing reduces the number of null
checks required in the code, which leads to smaller and faster code.
6. Error Handling
Here's a brief summary of PTS error-handling rules and built-in support:
-
There are two types of errors:
-
anticipated errors (e.g.
file_not_found_error
,invalid_data_error
) -
unanticipated errors (e.g.
out_of_memory_error
,stack_overflow_error
)
-
-
Anticipated errors:
-
are expected to possibly occur at run-time
-
must be declared as a function return type (using union types)
-
must be handled in one way or another, or explicitly ignored
-
-
Unanticipated errors:
-
are not expected to occur at run-time
-
can potentially occur at any time, and at any location in the source code
-
are either handled by one or more global error handlers or caught and handled explicitly anywhere in the source code
-
can be thrown implicitly or explicitly
-
-
A set of commonly used anticipated and unanticipated error types are defined in the standard library. Additional, domain-specific error types can be defined in a software development project, to cover specific needs.
-
PTS provides a set of dedicated operators and statements to facilitate error-handling.
7. We Should Eradicate the Empty String — Here's Why
Preventing immutable strings and collections from being empty provides the following advantages:
-
More reliable code because more bugs can be found at compile-time. The compiler reminds us to handle edge cases we might easily overlook.
-
Simpler and less error-prone code because there is no need to test for "
null
or empty". -
Simpler and less error-prone APIs for non-empty, immutable
string
and collection types (e.g.element_count
never returns zero, hence no risk of a division by zero).
These advantages are particularly valuable when working on large codebases.
The following approach is used in PTS:
-
Strings and collections are immutable and can't be empty.
-
Instead of empty strings/collections,
null
is used to represent "no elements". -
Builders are used to build (create) immutable strings and collections.
-
PTS also provides mutable strings and collections that can be empty, but they are used rarely.