F# 中有默认参数吗?

5

我写了一个程序用于将文件大小从字节转换为易读格式的F#代码:

let rec sizeFmt num i =
    let suffix="B"
    let unit = ["";"Ki";"Mi";"Gi";"Ti";"Pi";"Ei";"Zi"]
    match abs num with
        | x when x < 1024.0 -> printfn "%3.1f %s%s" num unit.[i] suffix
        | _ -> sizeFmt (num / 1024.0) (i+1)

let humanReadable n =
    sizeFmt (float n) 0   

运行示例:

> humanReadable 33;;
33.0 B
val it : unit = ()
> humanReadable 323379443;;
308.4 MiB
val it : unit = ()
> 

问题:

  1. 如果我想把 i=0 设置为 sizeFmt 函数的默认值,该怎么办?我查了 F# 文档,发现没有默认参数。所以我得写一个包装函数 humanReadable。有没有更好的方法?

  2. 为了处理像 humanReadable 123;;humanReadable 123433.33;; 这样的 int 和 float 类型输入,我不得不在包装函数中添加一个 float n。显然问题是:很容易超过最大的 int 大小,即 2,147,483,647。我猜应该有更好的方法,对吗?


1
你是否需要在某些时候使用强制深度?例如,42,000,000 -> "41,025.625 KiB"?还是它总是应该是"40.0543 MiB" - Be Brave Be Like Ukraine
该框架将文件大小报告为int64s;这是一种“更好的方式”。 - phoog
@bytebuster 应该是:小数点前不超过三位数字+最大适当单位。例如:40.1 MiB438.0 KiB249.8 GiB。(我使用了%3.1f进行格式化。) - Nick
@phoog 哪个框架?你能给我更多信息吗? - Nick
@Nick .NET框架。请参阅FileInfo.LengthFileStream.Length - phoog
可能是如何在F#中设置默认参数值?的重复问题。 - thalm
5个回答

6
如果sizeFmt仅由humanReadable使用,则将其作为内部函数是有意义的。这样可以避免“参数默认值”问题。

此外,标记外部函数inline会导致它接受任何支持显式转换为float类型的n类型。

let inline humanReadable n =
    let rec sizeFmt num i =
        let suffix="B"
        let unit = ["";"Ki";"Mi";"Gi";"Ti";"Pi";"Ei";"Zi"]
        match abs num with
            | x when x < 1024.0 -> printfn "%3.1f %s%s" num unit.[i] suffix
            | _ -> sizeFmt (num / 1024.0) (i+1)
    sizeFmt (float n) 0   

humanReadable 123 //works
humanReadable 123433.33 //also works

4

有一个可能有帮助的 F# 惯例,就是将主要参数放在参数列表的末尾,将次要参数放在前面 - 这与面向对象语言的惯例相反。这样可以让您将主要参数传递给函数,例如:

let rec sizeFmt i num =
   ...

123.0 |> sizeFmt 0

它还可以让您轻松创建带有填充可选参数的部分函数:
let humanReadable = sizeFmt 0

针对第二个问题,没有更好的方法,除非您将sizeFmt变成通用的,并传入1024.0的类型值,但这可能不会使它更简单。


2
唯一在F#中使用可选参数的方法是使用方法而不是函数。 要指定参数是可选的,请在其前面放置一个?。 从此处的文档中了解更多信息:
type DuplexType =
    | Full
    | Half

type Connection(?rate0 : int, ?duplex0 : DuplexType, ?parity0 : bool) =
    let duplex = defaultArg duplex0 Full
    let parity = defaultArg parity0 false 
    let mutable rate = match rate0 with
                        | Some rate1 -> rate1
                        | None -> match duplex with
                                  | Full -> 9600
                                  | Half -> 4800
    do printfn "Baud Rate: %d Duplex: %A Parity: %b" rate duplex parity

let conn1 = Connection(duplex0 = Full)
let conn2 = Connection(duplex0 = Half)
let conn3 = Connection(300, Half, true)

0

虽然我知道这不是被问到的内容,但你是否了解 F# 的 度量单位特性

[<Measure>] type B
[<Measure>] type kB

let bPerKB = 1024.M<B/kB>

let bytesToKiloBytes (bytes : decimal<B>) = bytes / bPerKB
let kiloBytesToBytes (kiloBytes : decimal<kB>) = kiloBytes * bPerKB

这提供了一种类型安全的方法来区分字节和千字节,并防止您意外将千字节值分配给期望字节的函数。
以下是一些示例转换:
> 1024.M<B> |> bytesToKiloBytes;;
val it : decimal<kB> = 1M
> 1145.M<B> |> bytesToKiloBytes;;
val it : decimal<kB> = 1.1181640625M
> 1.M<kB> |> kiloBytesToBytes;;
val it : decimal<B> = 1024M

如果你只是需要像上面那样的函数来快速将大字节值转换为人类可读格式,那么这肯定是过度设计了,但如果你需要在许多不同的比例尺下管理字节值,那么这可能是合适的。


0
现有的答案已经解释了保留包装函数是一个好主意,因为这样可以使代码尽可能模块化。在简单的示例中,这可能不是很明显,但在实际项目中,能够通过公开更多参数来扩展sizeFmt将是一个巨大的优势——考虑到您可能偶尔需要"Hertz"而不是"Bytes"(以及除以1000而不是1024),或者一个字符串格式模式(五个小数位),或者一个可本地化的乘数列表等等。
关于第二个问题,将其转换为float,解决方案非常简单:使value成为静态解析类型即可:
let inline humanReadable (value:^T) =
    sizeFmt (float value) 0

这将使humanReadable具有以下类型约束:
val inline humanReadable :
  value: ^T -> unit when  ^T : (static member op_Explicit :  ^T -> float)

使用方法:

humanReadable 42424242.42                      // float
humanReadable 4242                             // int
humanReadable 42424242424242424242I            // Numerics.BigInteger
humanReadable (424242424242424242422424N / 5N) // BigRational

内部函数中使用float似乎是可以的:任何舍入误差都将被一系列除法消除。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接