What would it take to add refinement types to Rust?
yoric.github.io
yoric.github.io
Units do prevent bugs in programs, so they have an important role to play. But they also need to be designed very carefully.
Java adopted units via JSR 385 (https://belief-driven-design.com/java-measurement-jsr-385-21...)
This probably doesn't have to be too complicated; the usual answer for Rust is "no". Rust doesn't even let you "just" add two unsigned integers of different sizes. Following that design, I would imagine units would require an explicit "turn feet into inches" or the other way around.
Rust is heavily inspired by Scala, but I guess achieving something like the examples in my post is difficult. I really hope Rust finds one way or another to make it work. Because simply forbidding everything all the time isn't even the safest way - it drives many people to just avoid it altogether and use unsafe code.
In fact this is the first time I've seen anyone say Scala influenced Rust let alone "heavily". Seems like a stretch
https://en.wikipedia.org/wiki/ML_(programming_language)
that ML influenced Rust, Scala, Haskell and OCaml, so that's the common denominator.
However, although Rust wants to be an ML style language all the idioms break because there's no GC.
In that overall the way option types, pattern matching, FP operations e.g. map/filter, immutability by default and type parameters have been implemented are very similar to Scala. And since these make up a large percentage of the everyday code you write you see the similarity as being larger than maybe it is.
And I think the fact the overall style of the language e.g. braces, semicolon, method signatures, loop handling is so similar to Scala versus OCaml contributes to this.
let v1 : Inches<Ratio> = Ratio::new(5, 8).into();
let v2 : Inches<Ratio> = Ratio::new(3, 8).into();
let v3 : Feet<_> = (v1 + v2).into();
assert_equal!(v3, Ratio::new(1, 12));And with a thoughtful approach to the API, you could avoid numerical error entirely by using integral types.
In Pascal these are called range types, e.g.
month: 1..12
would define an integer where the type system would ensure that it is always between 1 and 12.
Apart from Ada this seems to be an alien concept to all other languages. The concept of a "range type" also seems to have other meanings.
What is the Pascal "range type" properly called in type theory and what is its relationship to refinement types?
Subrange = range[0..5]
PositiveFloat = range[0.0..Inf]There's the concept of dependent types in e.g. Idris which lets us correctly do this sort of range check throughout the program (not just on literal assignment) but it comes with strict requirements on what the compiler can allow, such as no unbounded recursion, because checking dependent types is roughly equivalent to running the program.
If you mean to go all the way on units, dimensions and typing, there's a bestiary there, quited maintained (with several Ada 'takes' too) https://www.gmpreussner.com/research/dimensional-analysis-in...
How does Pascal handle overflow/underflow? E.g. Month 10 + Month 11 = Month 21?
$ cat a.pp
{$R+}
var
a: 1..12;
b: 1..12;
c: 1..12;
begin
a := 10;
b := 11;
c := a + b;
Writeln(c)
end.
$ fpc a.pp
Free Pascal Compiler version 3.2.2 [2021/05/19] for x86_64
Copyright (c) 1993-2021 by Florian Klaempfl and others
Target OS: Linux for x86-64
Compiling a.pp
Linking a
12 lines compiled, 0.1 sec
$ ./a
Runtime error 201 at $00000000004010D8
$00000000004010D8
$0000000000422EEC type
Month = enum Jan, Feb, Mar, Apr, May, ...
SpringMonth = range[Mar..May]
var m: SpringMonth = Mar
It will raise under/overflow exception if value falls out of range.e.g. { x:Int8 | 1 ≤ x ≤ 12 }
Similarly, if you have tuples (or structs with anon fields etc) in the language, you could unify those with arrays such that struct { int; int; } is the same as int[2].
They are very handy for preventing errors and expressing intent in Ada:
https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
E.g. with the article's example, where 'a' is Meters and 'b' is Seconds, 'a/(b*b)' and 'a/b/b' both have type 'Div<Meters, Mul<Seconds, Seconds>>' instead of one having the type 'Div<Div<Meters, Seconds>, Seconds>'.
For only Muls and Divs you can basically just have a histogram of units to powers (e.g., m/s^2 => m: 1, s: -2) which uniquely represent equivalent types.
If you’re looking at statistics for current or power the type system might try to convert it to joules even though you wanted to look at average wattage.
I guess I could see a naive implementation (confusing integration/sampling and discrete summation) going the other way, erroneously ending up with a nonsensical W/s.
Also don’t understand what it has to do with eponyms, which are just substitutes for base units, either your DA works or not, no? Average wattage is kg⋅m²⋅s⁻³ not kg⋅m²⋅s⁻² (joules) or kg⋅m²⋅s⁻⁴
Average wattage is measured in watts. You compute it by integrating and then dividing by the measure of the domain of integration.
Not the easiest framework to work with but pretty well thought out. A good starting point if you are looking to reinvent that wheel.
For example the unit Sievert is an official SI unit despite being just J/kg. This is because confusing equivalent dose and absorbed dose, which also has the unit of J/kg, could be very dangerous.
Note, that this is different from J sometimes being written as Ws. While there are informal conventions, when we use J and when Ws, using the unconventional one would not be technically wrong because 1 J is simply 1 Ws, whereas 1 Sv is not necessarily 1 J/kg when the later is an absorbed dose.
I think one could reasonably disagree with these decisions but that is how the SI people see it.
I'd make a stronger statement here; this is a specific example of when having units is most powerful. When even though two things are expressed in some common form, they nevertheless represent something different.
> Yeah, but equivalent units with different names is sort of only a display/formatting issue, right?
This could be said of two u32s as well.
This is where people go wrong trying to DRY and other refactors. Slightly forced example but
function averagePerClassroom(total) { return total / 30; } // 30 kids per class
function averagePerMonth(total) { return total / 30; } // assume 30
"Oh, the function body is the same therefore lets refactor this into an "averagePer" function" expect its two completely different concepts and once the code calculates the actual days per month or once classes are no longer 30 people suddenly things need to be un-refactored, or what I see more often, is just branching off inside the new single function based on an argument flag. Horrible.Adding energy to torque is rarely going to be intentional.
For example momentum is kilogram * meter / second, which is MASS^1 * LENGTH^1 * TIME^-1
As a vector, this can be represented as (1,1,-1) where the positions are M, L, T respectively.
In that format velocity is represented as (0,1,-1), acceleration is (0,1,-2), etc...
This is automatically canonicalised and much easier to manipulate than a tree of operations.
Of course, this assumes uniform units such as CGS or MKS in something sane like the metric system. Conversion back and forth is generally straightforward, as long as the types encode the system used. E.g.: CGS<1,1,-1> and MKS<1,1,-1> both represent momentum, but at different scales.
Imperial also works, and other base units can be added to extend the system. This can include things like current, temperature, moles, etc...
As long as the vector is sorted by unit, yeah. With that caveat, it's the same idea.
http://typesatwork.imm.dtu.dk/material/TaW_Paper_TypesAtWork...
I can make `type uuid = string` for self documentation, but a lot of plugins will just label it “string” and developers can (and have) mistakenly put some other identifier, like the robot’s hostname.
Of course we validate at the API but it’d be more skookum if we could prevent accidental wiring together of front-end components that make this error.
String literals help a ton. Gosh they’re wonderful to care about the shape of a string in the type system. But sometimes I really want to say “strict uuid” as in “I don’t care if it quacks like a duck, it’s not called duck.”
https://kubyshkin.name/posts/newtype-in-typescript/
Unfortunately doesn’t help much when you’re dealing with functions from packages someone else has typed.
You basically define a `type A = string &{a: SomeSymbol}`
And then have a type assertion function that just returns true, and you have control over the places A can come from
I assume the idea is to lie to the type system about the existence of the symbol, and at runtime it is just a string.
type FooID = string & { typeName? : "FooID" }
Read as 'of course this thing doesn't have a typeName[1] property, since it's a string, but if it did have the property, the value would be "FooID"'. You can then cast between FooID and string, but not between FooID and some other type that declares a typeName property.[1] I actually tend to use 'classRef' with an RDFish long name for the type, but that makes examples longer and isn't the point.
I think I’ll also try to base it on a template string too if that’s possible. Given we use a standard dash segmented uuid string.
Examples of double-refinements that I'd like:
- Common units like Length:Meter and Length:Foot.
- Color bits like Color:RGB24 and Color:CYMK24.
- Worldwide currency like Money:USD and Money:GBP with a converter function that knows exchange rates.
- Human languages like String:English vs String:Cymraeg with a converter function that knows translations.
Exchange rates vary over time, so you'd arguably need a type which includes a timestamp (e.g. "USD 1000 at 2024-12-25").
And that's ignoring all these other complexities such as the spread, different currency converters offering differing rates, unofficial and multiple official rates in countries with currency controls (e.g. Argentina), hedging, etc
One such case is customs agencies publishing their own exchange rates for use in custom declarations, for example here[1] for the US or here[2] for Sweden.
[1]: https://www.cbp.gov/trade/document/report/daily-foreign-curr...
[2]: https://tulltaxan.tullverket.se/arctictariff-public-web/#!/t...
def Nat = Int: (|x| -> x >= 0)
Dependent types allow types to be computed from functions (and depend on arguments, otherwise it seems they become just weird constants), def Five(as_type: String) -> NumericType(as_type):
match as_type:
"string" => "five"
"int" => 5
"float" => 5.0f
"double" => 5.0d
_ => panic() // Unnecessary if you refine `as_type` from a String to an enum or a fixed set of strings.
Dependent types seem weird, but they help making types first-class (https://www.youtube.com/watch?v=mOtKD7ml0NU&t=325s) and gaining types like `Array<T, N>` that allow ensuring things are the right length, and define append/extend properly.Like if `(Nat 5) - (Nat 6)`. Or is subtraction disallowed?
let x = Nat 5;
let y = read_nat();
assert y < x; // without this, the following would fail to type check
x - y
(you need occurrence typing, too, in this example)In terms of numbers, what you can expect from refinement types is similar to what you get with CLP(FD) in Prolog.
Maybe some operations can be proved to stay within the refined type, like adding natural numbers, but that's something that the used would need to provide as a function allowing that under assertions or some proof that the compiler can verify and trust.
I just realized my lambda syntax on the Nat predicate is redundant because I didn't clean up and that using snake_case for function names would be better in a language that lets you operate on functions like they are values.
Refinement types typically(1) refers to a type systems that lets you create a subtype of a type through refining (qualifying) with a predicate or constraint on the shape. Examples {x \in int | is_even x } or { x \in List | len(x) = 1 }
Refinement types can be very powerful but that may well make type checking undecidable (think of a type of Turing machines, and the refinement that keeps only the ones that halt). By being careful about the logic used in the refinements, one may retain decidability.
(1) The article seems to have a different idea of what a refinement type is: quote "a type system that does its work after another type system has already done its work".
I am not going to play orthodox guardian of type theory terminology here, yet to me personally, it does seem unfortunate to use that term. The author seems to really want a form of type-level computation, which could be interesting if it could be rigorously specified and it's relation to the existing type level reduction clarified.
And there are more examples of that, I can have probabilities from different distributions, I don't want to add them accidentally, though multiplications of them is all over the place, so let it be. Of course I have no hope that any language could deal with the explosion of types that are needed to represent this, but if compiler just gave me f64 as a result of multiplication of probabilites, I'd be happy.
It can be done in Rust on case by case bases, but it is a lot of boilerplate. The issue is the typedef IntProb = u32, treat IntProb like an alias to u32 and rustc converts these types into each other silently like C compiler converts int to char. One can do struct IntProb(u32), but then it would be needed to implement traits like Add, Mul, Cmp and so on, which is possible but it is too much work. If it was possible to force rustc to treat typedef MyType = {NumericType} as a distinct numeric type that requires explicit conversion into NumericType, while retaining all the traits of NumericType, just substituting in them NumericType with MyType, it would be great.
I think, that all the complexities described in the article stem from the attempt to create an universal instrument that can do everything and to keep bees. The real difficulty is to pick a small subset of wants, that will cover 80% of needs, while being really simple. I see no issues with occasional .into::<Length<f64>>(), like I see no issues with (my_struc.index as usize), if I keep index as u8 to spare memory, but use it as usize of course. I see no need in different units for the same quantity, because in any case I'd want to convert everything into the same uniform units before I start adding and multiplying. But I'd like to have restrictions on available operations between different types, and I'd like to have a possibility to add optional dynamic checks for type value, that I could turn on for a debug build and turn off for a release one. For example, I'd like to check that any probability p fits into [0, 1].