简体   繁体   中英

Defining generic conversion to and from measurement units in F# to their underlying types

It's easy enough to convert from a measurement unit to the underlying value type and back:

[<Measure>]
type meter

module Meter =
    let from m = m * 1</meter>
    let too m = m * 1<meter>

But then I thought, wouldn't it be lovely to have a conversion from one underlying type to the measurement unit and back, generically? Sure, I can on each instance be specific about it but I figured it'd be convenient to have a cast-to and cast-from operator, while retaining type safety.

My first attempt:

[<AutoOpen>]
module Convert = 
    let inline (!->) x = x * 1<_>    // (often) magically works
    let inline (!-<) x = x * 1</_>   // syntax error on the "/_"

The !-> operator works by virtue of type inference. It would be nice if the second operator also worked, but /_ is not supported syntax. I tried some type-annotated variants but couldn't come up with a straight generic solution, in part because we cannot write 'T<'M> or some variant thereof.

I then thought of using the trick used for implicit conversions, but then applied to explicit conversions, since the underlying type of 'T<'M> if 'T and an explicit cast always exists:

[<AutoOpen>]
module Convert = 
    let inline explicit_convert (x:^a) : ^b = ((^a or ^b) : (static member op_Explicit : ^a -> ^b) x) 
    let inline (::>) x = explicit_convert x       // works both ways

This has the added benefit that it works on any type that has an explicit conversion operator. Unfortunately, the compiler complains that it may not be safe to do so:

Member constraints with the name 'op_Explicit' are given special status by the F# compiler as certain .NET types are implicitly augmented with this member. This may result in runtime failures if you attempt to invoke the member constraint from your own code.

I'm not sure if I'm on the right track or whether there's a more type-safe way of creating an operator that can go from any measurement to the underlying type and back (including complex measurements, like int<meter ^ 4> or int<meter / sec ^ 2> )

LanguagePrimitives.FloatWithMeasure , Int32WithMeasure , etc methods are provided in the runtime, which will generically "unitize" a value.

Similarly, float , int , etc functions (normally used for casting) will strip off a generic unit.

The tricky bit is seamlessly binding to the right functions at compile time in a general way. Below does what I think you want, following the approach shown here , which I believe is the standard trick for this kind of stuff.

[<AutoOpen>]
module Convert = 

  type AddUnits = AddUnits with
      static member ($) (AddUnits, x: float) = LanguagePrimitives.FloatWithMeasure<_>(x) 
      static member ($) (AddUnits, x: int) = LanguagePrimitives.Int32WithMeasure<_>(x)

  let inline (!->) x = AddUnits $ x

  type StripUnits = StripUnits with
      static member ($) (StripUnits, x:float<_>) = float x
      static member ($) (StripUnits, x:int<_>) = int x

  let inline (!-<) x = StripUnits $ x

open FSharp.Data.UnitSystems.SI.UnitSymbols

!-< 22<m>       // 22
!-< 9.8<m/s^2>  // 9.8

let x : int<m> = !-> 22         // 22<m>
let y : float<m/s^2> = !-> 9.8  // 9.8<m/s^2>

You have actually already written the function to remove the units of measure - see this example:

> let t:int = !->1<m>;;

val t : int = 1

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM