6

如何利用F#的辅助让.Net编程变得高效

 3 years ago
source link: https://zhuanlan.zhihu.com/p/137774395
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何利用F#的辅助让.Net编程变得高效

正在找工作⎝

前面几篇,我给大家分享了F#使用一些技巧做一些有趣的代码实现。然而应用F#并不总是需要那么复杂的技巧。下面我来介绍F#的一些小功能集锦,告诉你如何让开发变得轻松:

一、多使用管道符|>

F# 内部实现了一个符号|>,其实现很简单:

let inline ( |> ) x f = f x

然而它却神奇地让代码的读写变得轻松,譬如,下面由右往左执行的代码:

let fastGun x =
    face.bukkake(hole.cram(clothes.Off(x)))

使用|>之后就可以写成这样:

let fastGun x =
    x |> clothes.Off 
      |> hole.cram
      |> face.bukkake 

其实|>(pipeline) 应该翻译成流水线

它完美地诠释了一个产品从开始制作到最终出品经过的每一道工序,并且没有使用中间变量。

扩展使用:

我们还可以顺着这种思路,扩展一下,譬如,我定义一个新的符号:

let inline (|>>>) x f =
    #if DEBUG
        let x = f x
        log x  // 写入日志
        x
    #else
        f x
    #endif

然后在某个地方替换 |>,它就可以在调试时候把中间状态打印出来:

let fastGun x =
    x |>>> clothes.Off // 调试
      |> hole.cram
      |> face.bukkake 

二、多写一些帮助性的小函数。

F#最吸引人的地方就是可以尽量减少类型以及泛型约束的无谓声明,这在我设计一些复杂的函数演算时尤其重要,因为输入输出的类型我都还没定好呢,你让我先写类型,太难为我了。而更常用的是接口化编程时候,往往接口的约束比代码还长,这样的类型声明避无可避,但是使用一些辅助函数可以帮我们节省很多精力,譬如:

    let dispose (value: #IDisposable) = value.Dispose()

#IDisposable是F#为了我们更方便使用类型,而提供的最后一点便利,它相当于:

    let dispose<'T when 'T :> IDisposable> (value: 'T) = value.Dispose()

当然,有时候不得不用详细的语法,我们再声明一个辅助函数,就得这样:

let eq<'T when 'T :> 'T IEquatable> (a: 'T) (b: 'T) = a.Equals b

现在我们已经有两个小函数,但是它们怎么起到帮助的作用呢,请看以下代码:

let disposeIfMatch x y =
    if eq x y then
        dispose y

F# 通过类型判断,就知道 x 必须具有接口 IDisposable IEquatable<T> 而 y 与 x 同类型,编译时F#也会把小函数优化成直接调用:

把一些常用的接口方法写成辅助函数,往往可以让我们事半功倍。F#提供的#Type语法可以让我们封装时候非常轻松。

三、适当使用活动模式匹配。

某些时候使用活动模式匹配来替代函数,会让代码表达更简洁,譬如:

let int32try (x: string) =
    match Int32.TryParse x with
    | true, x -> x
    | _ -> -1

let foo (x: string) =
    match x with
    | null | "" -> -2
    | _ -> match int32try with
           | 1 -> 1
           | 2 -> 3
           | _ -> 10
    

这里函数foo 使用了两层match,但是如果我把函数int32try改成用活动模式来实现:

let (|Int32|) (x: string) =
    match Int32.TryParse x with
    | true, x -> x
    | _ -> -1

那么,foo则可以使用混合匹配写得更简单一点:

let foo x = 
    match x with
    | null | "" -> -2
    | Int32 1 -> 1
    | Int32 2 -> 3
    | _ -> 10

现在就只有一层模式匹配,翻译过来的代码如下:

public static int foo(string x)
{
	if (x == null || string.Equals(x, ""))
	{
		return -2;
	}
	switch (|Int32|(x))
	{
	default:
		return 10;
	case 1:
		return 1;
	case 2:
		return 3;
	}
}

这里没有多余的函数调用。这里有一点小诀窍:

如果你使用

match x with
| Int32 x when x ....

匹配变量时,最多使用一次,否则多一次使用,就会多一次函数调用。所以尽量使用文本值匹配,注意副作用的影响,另外请注意,我建议的不是「部分活动模式匹配」。

补注:如果一个活动模式匹配返回Unit,那么该匹配可以用做最后的匹配,并且不需要任何常量或变量匹配。看以下例子:

let (|Unhandled|) (ex: Exception) =
    printfn "%A" ex

以上声明了一个未处理异常的活动模式。那么我们可以在下面这样使用:

let foo a =
    try 
        100.0 / a
    with 
    | Unhandled -> Double.NaN

它就可以把所有未处理的异常打印出来,执行foo 0,就打印出:

System.DivideByZeroException: 尝试除以零。
在 FSI_0015.foo(Int32 a)

四、把模块里的函数编译成C#可用的扩展方法。

F# 里可以写出C#可调用的扩展方法。微软文档 里提示的是用类来实现。但是其实C#判断是否扩展方法的规则很简单,只要类型上面有[Extension]批注,方法上面也有[Extension]批注就判断为扩展方法。简而言之,即使是ref struct只要批注上了,也能辨别出来。 所以我们完全可以把模块里面的函数编译成C#可用的扩展类型。这样的好处是,我们还可以继续享用模块里柯里化函数的便利,例子如下:

[<Extension>]
module Extensions = 
    [<Extension; CompiledName "Max">]
    let max(a: int) b =
        if a >= b then
            a
        else 
            b

现在C#里就可以看到 Max 这个扩展方法,而F#里继续使用柯里化的maxCompiledName

是可选的,它表示编译成IL时使用什么名字。为了C#习惯,当然是使用大写了。 另外,我们还可以通过CompileName把不同的函数编译成重载函数。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK