dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
290 stars 63 forks source link

Units of Measure #120

Open AnthonyDGreen opened 7 years ago

AnthonyDGreen commented 7 years ago

This seems like it could be the kind of near-domain-specific feature that would benefit VB developers. This could useful to a host of practical applications. F# really sells the feature well here so hopefully we could borrow their design mostly intact (wouldn't it be even better if we could use F# libraries as-is?).

Right now, the syntax that seems the least ambiguous is the backtick:

Obviously this is awesome for units of length and weight:

Dim feet = 12`in
Dim ounce = 1`oz
Dim pound = 16`oz
Dim gallon = 4`qt

This might also be awesome for units of currency:

Dim minPrice = 1.00`USD

And maybe even other less concrete things like flavoring primitive types:

Dim count = arr.Length`Count
For i = 0`Index to CType(count, Index)

Dim fs = "Hello, {0}"`FormatString

Things like counts vs indexes, localized vs non-localized strings, byte encodings, etc can be represented an enforced by units of measure rather than Hungarian notation (no offense to our Hungarian sisters and brothers--we love you Hungary! <3)

I don't have a particular syntax for defining a unit in mind.

One thing I've been pondering is how this feature gels with VBs loose type conversion semantics. I've decided that even with Option Strict Off the feature isn't wasted because:

1) impossible conversions will still give errors 2) we could special case it if we wanted since it's a new thing 3) a big benefit of the the feature isn't so much forcing you to write out conversions but ensuring that arithmetic is done correctly for the units involved. Put another way, it doesn't matter if the compiler converts inches to millimeters before adding, it matters that the conversion happen before adding. Even if it happens implicitly you're still getting a numerically correct result. 4) Since often conversions in both directions would be valid it might just be an ambiguity for the compiler to choose anyway.

Echo-8-ERA commented 7 years ago

If you use a single back tick, how would compound units work (e.g. m/s)?

AnthonyDGreen commented 7 years ago

Well we'd definitely want them to look like that m/s. I'm not sure there's any ambiguity. 9.8m / 1s = 9.8`m/s but I honestly haven't thought through a lot of examples to be sure. I wanted to put the idea of units of measure out there to see if others think it would help make programs safer. Do you see mistakes with unit conversions?

Echo-8-ERA commented 7 years ago

TBH, when I posted, I had just briefly looked at the F# docs and wondered why they used a pair of angle brackets to delimit instead of single character like here, since as you say, you can decompose compound units into multiple operations involving their component units.

McZosch commented 7 years ago

There are two ways such a feature could work,

The first case would be to handle units as variable annotations, which the type and the control flow checker use to find errors. At runtime, any notion of units would vanish, which means: no guaranteed checks at runtime. It would also be strictly a language feature, that would be hard to extend.

The second possibility is a unit-library, which then needs a couple of language features to work as advertized. I've written an Unit-implementation myself, using a Unit-basetype, a couple of different intermediate types to separate length, weight, etc., and which also form base types for other units (f.e. mile inherits inch, the base length type). Every intermediate class is the host of operators, so every length gets add up as inch and needs to be properly converted.

To lower the bar a little, a simple feature would be required, which would be handy anyways: Arbitrary operators; in this case the "'in"; which is then expressed as:

Public Shared Operator 'in(value As Double) As Inch
Public Shared Operator 'mi(value As Double) As Mile
Public Shared Operator 'mi(value As Inch) As Yard
Public Shared Operator 'mi(value As Inch) As Mile

a.s.o.

If all operations are only in the inch-unit, it would work as follows:

Initialization Dim l = 5.62'mi Pow (returns square inch) Dim q = l ^ 2
Simple conversion of mile into yard Dim i = l'yd Conversion of math result into nautical mile, which would otherwise be inch Dim t = (i + l)'nm

Btw, currency is a corner case, because its conversions are not fixed, but get calculated daily / continuously. Another such case is Pixel, where knowledge of Dpi is required to convert to Inch.

AdamSpeight2008 commented 7 years ago

@McZosch I don't think postfix unary operators are supported by VB.net

gilfusion commented 7 years ago

Just to add to the conversion conversation, in F#, a unit of measure can be applied to a value of any signed integer or floating-point type. You could, I think, set up meter as a unit of measure and have some code using integer lengths in meters and other code using floating-point lengths in meters. How would this affect conversions, particularly ones without a nice integer conversion factor, like between feet and meters? Should it be possible to set up multiple conversions overloaded based on the underlying type, or should a unit conversion, for example, convert the underlying value to Double, run the actual unit conversion calculation, then convert the result back to the original underlying type?

Syntax-wise, could something generic-like work, since the built-in integer and floating-point types are not generic?

Function CalcRectangleArea(width As Integer(Of in), height As Integer(Of in)) As Integer(Of in ^ 2)
    Return width * height
End Function
Dim area = CalcRectangleArea(6(Of in), 2(Of ft))

'More complex example
Function CalcGravityForce(mass1 As Double(Of kg), 
                          mass2 As Double(Of kg), 
                          distance As Double(Of m)) As Double(Of N)
    Dim G = 6.674E-11(Of N * m ^ 2 / kg ^ 2) 'Gravitational constant
    Return G * mass1 * mass2 / distance ^ 2
End Function
Dim force As Double(Of N) = CalcGravityForce(1.2(Of kg), 1.3(Of kg), 2.5(Of m))
McZosch commented 7 years ago

@AdamSpeight2008 Don't know, if having postfix operators is a huge problem. Any math operator to a degree is a postfix to its first operand.

Maybe the model could be derived to have a general conversion or as operator, which is a rewrite of the normal CType-logic. This way, we could write:

Dim l = 5.62 as mi

which would do the same as

Dim l = CType(5.62, mi)

but is much more readable.

Bill-McC commented 6 years ago

Don't like it. Would rather see a set of classes/structs as in keeping with timespan, rect or point etc If you have a struct Distance, you could set/get the length in different supported units, .Metres, .Kilometres, .Feet, .Inches, etc, just like you can refer to a TimeSpan TotalSeconds. And you could have an Area type that takes two Distances, and a Volume type that takes three Distances. Likewsie and Angle type that has Degrees, Radians, and Grad. And so on.

I do not want them baked into the language, especially not currencies. Have had too many problems with DateTime's in the past. And imagine what happens if a currency is renamed, a country divides ?

AdamSpeight2008 commented 6 years ago

@AnthonyDGreen I like the concept of Unit Of Measure but not the proposed syntax, see alternative syntax #200

ericmutta commented 6 years ago

@AnthonyDGreen @AdamSpeight2008 ...just passing through here, haven't read all the comments above, but wanted to share how I am handling this right now:

    <Extension>
    Public Function KB(ArgValue As Integer) As Integer
      Return Math.Abs(ArgValue) * 1024
    End Function

    <Extension>
    Public Function MB(ArgValue As Integer) As Integer
      Return Math.Abs(ArgValue) * 1024 * 1024
    End Function

The above extension methods allow me to write code like this (thanks to VB's ability to invoke methods without requiring brackets):

Dim PageSize = 4.KB
Dim PagesPerMB = 1.MB \ PageSize

It seems possible that we could implement units of measure in VB as it stands today by using a library of specific types of measures (e.g. length type, weight type, etc), along with operator overloading and extension methods.

Food for thought!

AdamSpeight2008 commented 6 years ago

@ericmutta That could be way to produce a unit of measure, but returning an Integer defeats the point. You lose the contextual significance of the unit of measure.

It could be a implement as set of classes (see #200), but would it be good to have the compiler implement them for you.