F#奇妙游(8):计量单位

F#的计量单位

从各个意义上,都可以说计量单位很重要。度量衡的统一是社会化大生产和市场交换的基石。这句正确的废话跟F#没有半毛钱的关系,直到我看帮助看到了这个:

[<Measure>] type m
let l = 1<m>

脑袋顿时秃起来哦不兴奋起来。仔细看了看几个帖子,发现这个东西是F#的一个特性,叫做Units of Measure

其实网上提的不太多,本来F#的内容就少。

为什么需要这个计量单位

这个特性看起来很美好,编程序的时候能够把单位写在变量的后面,可以自动检查单位的正确性,比如:

// open FSharp.Data.UnitSystems.SI.UnitSymbols
// 或者自己定义下:
[<Measure>] type m
[<Measure>] type s
let speed = 10<m/s>
let time = 5<s>
let distance = speed * time

这里距离自动推导出距离的单位m,而且speed * time的结果也是m,如果写成speed + time就会报错,因为单位不匹配。

好多帖子里面还提到NASA把单位搞错,导致火箭坠毁,这个我没仔细去找相关的资料。不过老美那个没文化的,全球都在搞国际单位制,就他抱着英制不放,看NACA(NASA早期的名字)的文章简直是费了劲。

只是这个单位写法和用法,都是编译期的,在运行期间,这个单位就没有了。没有办法访问,没有办法转字符串,没有办法获取和判断。唯一的用处就是在编译时候检查单位的正确性。

这么好的奇淫巧技,微软到底是怎么让它进入F#的呢?我思考了非常久,也没有想明白。因为这个东西这么好的话,C/C++/Java肯定早就要搞起来了啊?为什么呢?

为什么C/C++/Java没有这个计量单位的功能

我想了很久,也没有想明白。我觉得可能是因为这个东西太复杂了,而且用处不大。比如我写了一个函数:

let f (x:float) = x + 1.0

这个函数的输入是float,输出也是float,那么我在调用的时候,就不需要写f 1.0f,因为编译器会自动推导出来。如果我写成:

let f (x:float<m>) = x + 1.0<m>

有两种可能:

  1. 通过命名约定、检查工具、单元测试,不管是什么东西,在别的主流工程语言中,这个问题不复存在。所以这个功能就没有必要了。
  2. 这个功能实现起来跟人类实现度量衡统一一样难,最聪明的物理学家,到现在连功的单位都没有统一,简直是人类之耻。
  3. F#做这个功能实在是太简单?所以语言实现的胸弟随便就做上了?

为了开心和不开心,我还是把跟这个相关的东西都看了一遍,头发少了好几根。

真正有用的Unit of Measure

说这个玩意没有用是一方面,说计量单位没有用那就是胡扯。有一个很好的包叫做CoolProp,可以计算各种物质的物理性质,比如下面的代码输入是温度和压力,输出是密度。这个密度的单位是kg/m^3

let water = CoolProp.PropsSI("D","T",300.0,"P",101325.0,"Water")

这个包C++编的,提供了.NET接口,叫做SharpProp,用F#可以直接调用。

let water = SharpProp.PropsSI("D","T",300.0,"P",101325.0,"Water")

这么下起来是不是很没有牌面?

.NET程序员(准确地说是C#程序员)大神,做了一个很有用的单位系统。这个东西叫做UnitsNet。这个东西是用C#写的,但是F#也可以直接调用。

open System
open SharpProp
open UnitsNet.Units
open UnitsNet.NumberExtensions.NumberToPressure
open UnitsNet.NumberExtensions.NumberToTemperature
open UnitsNet


let fluid (f: FluidsList) (p: float) (T: float) =
    (new Fluid(f))
        .WithState(Input.Pressure(p.Pascals()), Input.Temperature(T.Kelvins()))

type Fluid with
    member this.density = this.Density.KilogramsPerCubicMeter
    member this.pressure = this.Pressure.Pascals
    member this.temperature = this.Temperature.Kelvins
    member this.specificHeat = this.SpecificHeat.JoulesPerKilogramKelvin

    member this.viscosity =
        match this.DynamicViscosity with
        | v when v.HasValue -> v.Value.NewtonSecondsPerMeterSquared
        | _ -> Double.NaN

    member this.kinematicViscosity =
        match this.KinematicViscosity with
        | v when v.HasValue -> v.Value.SquareMetersPerSecond
        | _ -> Double.NaN

let density (f: FluidsList) (p: float) (T: float) =
    (new Fluid(f))
        .WithState(Input.Pressure(p.Pascals()), Input.Temperature(T.Kelvins()))
        .Density.KilogramsPerCubicMeter

let specificHeat (f: FluidsList) (p: float) (T: float) =
    (new Fluid(f))
        .WithState(Input.Pressure(p.Pascals()), Input.Temperature(T.Kelvins()))
        .SpecificHeat.JoulesPerKilogramKelvin

let viscosity (f: FluidsList) (p: float) (T: float) =
    match
        (new Fluid(f))
            .WithState(Input.Pressure(p.Pascals()), Input.Temperature(T.Kelvins()))
            .DynamicViscosity
    with
    | v when v.HasValue -> v.Value.NewtonSecondsPerMeterSquared
    | _ -> Double.NaN

let kinematicViscosity (f: FluidsList) (p: float) (T: float) =
    match
        (new Fluid(f))
            .WithState(Input.Pressure(p.Pascals()), Input.Temperature(T.Kelvins()))
            .KinematicViscosity
    with
    | v when v.HasValue -> v.Value.SquareMetersPerSecond
    | _ -> Double.NaN

[<EntryPoint>]
let main argv =
    for T in 280..10..350 do
        let water = fluid FluidsList.Water 101325.0 (float T)

        printfn
            $"%20f{water.density} kg/m³%20f{water.pressure} Pa%20f{water.temperature} K%20e{water.viscosity} Pa·s%20e{water.kinematicViscosity} m²/s%20f{water.specificHeat} J/kg·K"

    printfn ""

    for T in 280..10..500 do

        let air = fluid FluidsList.Air 101325.0 (float T)

        printfn
            $"%20f{air.density} kg/m³%20f{air.pressure} Pa%20f{air.temperature} K%20e{air.viscosity} Pa·s%20e{air.kinematicViscosity} m²/s%20f{air.specificHeat} J/kg·K"
    0 // return an integer exit code

很简单的就能把各个单位在运行期的行为都统一起来。这个东西是真正有用的。

UnitsNet的功能举例

UnitsNet把一个带单位的量描述为(数值,单位)的组合,不同的单位之间的转换都在运行时可以获得,各单位的名称也都是在运行时可以获得的。这个东西的功能非常强大,可以用来做各种单位转换,比如下面的代码就是把压力从帕斯卡转换为其他单位。

open UnitsNet
open UnitsNet.Units
open UnitsNet.NumberExtensions.NumberToPressure
// 用不同的单位定义一个量
let p = 101325.0.Pascals()
let p1 = 1.0.Atmospheres()
let p2 = 760.0.Torr()
let p3 = 14.695948775513.PoundsForcePerSquareInch()
let p4 = 1013.25.Millibars()
let p5 = 1013.25.Hectopascals()
// 把一个量转化为不同的单位
printfn $"%f" p.Atmospheres
printfn $"%f" p.Torr
printfn $"%f" p.PoundsForcePerSquareInch

总结

  1. F#的Unit of Measure是一个很有意思的功能,但是实际上没有什么用。
  2. UnitsNet实现的单位系统是真正有用的。
  3. F#调用C#的库还是很好很强大的。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大福是小强

除非你钱多烧得慌……

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值