Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert between Unit of Measurement by means of conversion expressions #911

Closed
3 of 5 tasks
ajeckmans opened this issue Sep 1, 2020 · 7 comments
Closed
3 of 5 tasks

Comments

@ajeckmans
Copy link

ajeckmans commented Sep 1, 2020

Convert between Unit of Measurement by means of conversion expressions

After seeing a discussion on Reddit and searching the issues here I propose we allow for compound units of measurement. Take for example this code

[<Measure>] type meter
[<Measure>] type second
[<Measure>] type hour

let speed distance time =
    distance / time

let distance speed time = 
    speed * time

let calculatedSpeed = speed 100<meter> 1<second> // -> 100<m/s>

distance calculatedSpeed 1<hour> // --> 100<h m/s>

// proposed
[<MeasureConversion>] let hour -> seconds = hour * 3600

distance calculatedSpeed 1<hour> // -> 360000<meter>

Once you get in the realm of more complicated units like N m/s and multiplying those with kN km/h and such I think the real benefits will start to show.

The existing way of approaching this problem in F# is to define a conversion method to do this calculation for you and call this manually each time you want the conversion to happen and to create these conversion functions for converting between all possible units, whereas with this maybe something like the following will be possible

[<Measure>] type meter
[<Measure>] type kilometer
[<Measure>] type centimeter

// proposed
// convert down
[<MeasureConversion>] let kilometer -> meter = hour * 3600 
[<MeasureConversion>] let meter -> centimeter = hour * 3600

// convert up
[<MeasureConversion>] let centimeter -> meter = hour * 3600
[<MeasureConversion>] let meter -> kilometer = hour * 3600


let mutable size = 1<centimeter>

size <- 1<kilometer> // -> 360000<meter>

val size : int<sentimeter> = 100000<centimeter>

Pros and Cons

The advantages of making this adjustment to F# are making UoM more expressive and code easier to read.

The disadvantages of making this adjustment to F# are yet another feature which will make the language more expressive, but also more complicated.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): probably not trivial to implement.

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate (I've searched but could not find something like this yet)
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@ajeckmans
Copy link
Author

To be honest I made this proposal mainly to start a discussion and to have the conclusion documented :) The more I think about it, the more I actually like the idea, but also the more I think this is quite unfeasible to do.

@abelbraaksma
Copy link
Member

abelbraaksma commented Sep 1, 2020

I've searched but could not find something like this yet

I believe this is covered by this proposal #892, which aims to make it easier to convert to/from measures. If not, please elaborate in what it does differently (I'm aware that I may just not understand your suggestion well enough ;)

@ajeckmans
Copy link
Author

ajeckmans commented Sep 1, 2020

@abelbraaksma I believe that proposal is about converting to and from the primitive so int -> int<m> and int<m> -> int or between underlying primitives, so int<m> -> float<m>. This one is about conversions between UoMs like int<m> -> int<km> and int<km> -> int<m>.

I propose we allow the conversions between nthose UoMs to happen automatically when a conversion expression is defined. So anytime the compiler expects an int<m> for a function and is given an int<km> when a conversion between those types is defined it can automatically call into that conversion.

AFAIK this is not yet possible and the compiler will not allow something like this:

let speed distance time = 
   distance / time

speed 1<m> 1<s>  // -> 1<m/s>

speed 1<km> 1<s> // compiler error

And with my limited understanding there's only one proper way to do this currently

let inline convertToMeter <kilometer:int<kilometer> : int<meter> =
   (kilometer |> int |>LanguagePrimitives.IntWithMeasure) * 1000

let speed distance time = 
   distance / time

speed 1<m> 1<s>  // -> 1<m/s>

speed (convertToMeter  1<km>) 1<s> // -> 1<m/s>

this starts to break down when you also want to arbitrarily convert km -> m -> cm -> nm etc. As you need to define all conversions upfront
km -> m
km -> cm
km -> nm
m -> cm
m -> nm
cm -> nm

whereas if you can somehow define a measurement in terms of another measurement the compiler can figure out how to do the conversion for you.

I realize this might break with the don't implicitly convert, but maybe a syntax can be added for that as well, to opt-in to the conversion per statement, for example:

let speed distance time = 
   distance / time

speed 1<m> 1<s>

speed 1<km>* 1<s>

where * would obviously need to be something that is not yet in use.

@ajeckmans ajeckmans changed the title Compound Unit of Measurement by means of conversion expressions Convert between Unit of Measurement by means of conversion expressions Sep 1, 2020
@abelbraaksma
Copy link
Member

I understand it better now, thanks. Given the other proposal, this would be easier to define, but I agree, it wouldn't be automatic. I'm not sure I personally agree to the implicit nature (this could cause backward compat issues, maybe), and perhaps it would be better to allow defining this on the measure type itself.

But the idea is intriguing now that I understand it better :)

@ajeckmans
Copy link
Author

There is indeed some overlap between the proposals. Whereas yours deals mainly with conversion, this one is maybe more about being able to define one UoM in terms of another through some means in such a way that the conversion can help you eliminate tedious manual conversions (possibly even at compile time).

@dsyme
Copy link
Collaborator

dsyme commented Jun 14, 2022

I understand the suggestion, however this was effectively decided in F# 2.0 - units of measure do not allow equivalence-up-to-conversion, and so units like hour and second are considered unrelated, and you must value * 3600<second/hour> to convert. The recommendation is to follow a discipline for naming and using such conversion constants.

It is a deep alteration to UoM to allow scaling , and while the specific language feature above to declare scalings is not a bad suggestion, it's just a fundamental reworking of the assumptions underlying the whole feature.

@T-Gro
Copy link

T-Gro commented Nov 30, 2022

What if the units were still declared and considered as independent, but the ergonomics of converting them by user code would be eased by the use of implicit conversions?

The following code works as of today, but of course it is not nice to use.

  • The conversion would have to happen implicitly
  • There would be mechanism to declare the op_Implicit scaling agnostic to numeric type
  • Right now multiple such conversions would not work, because they are considered equal after erasure. They would need to be marked as F# only and inlined.
[<Measure>] type meter
[<Measure>] type km = static member op_Implicit(meters : float<meter>) = meters / 1000.<meter/km>

[<Measure>] type second
[<Measure>] 
type hour = static member op_Implicit(seconds : float<second>) = seconds / 3600.<second/hour>

let speed distance time =distance / time

let mySpeed = speed  1.<km> 1.<hour>  + speed (km.op_Implicit 1.<meter>) (hour.op_Implicit 1.<second>)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants