13

F# 轻松实现依赖注入

 3 years ago
source link: https://zhuanlan.zhihu.com/p/137460149
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# 轻松实现依赖注入

正在找工作⎝

现在http://Asp.Net Core用依赖注入用得很爽,我又有了一个大胆的想法。

F#的inline这么强大,能不能在不使用反射、Emit等动态方式的情形下实现依赖注入呢?闲话少说,赶紧动手。

首先有个愿景,做成怎样的依赖注入呢?应该是这样:对于一个有函数,只要填入框架支持的参数,不论输入顺序,只要调用Dependency.Inject就能把已有的实例填进去。 譬如,这样一个函数:

// 打印参数类型和它的值
let Foo(a: int) (b: int64) (c: DateTime) = 
    printf "%A:%d\n%A:%d\n%A: %A" (a.GetType()) a (b.GetType()) b (c.GetType()) c 

只要我调用:

Dependency.Inject(Foo)

就可以把准备好的实例填入做参数,并执行(简化一下,复杂的可以再扩展)。主要是Dependency.Inject该如何实现 ?函数的参数数量不等,而且顺序又不定。很自然,我们又回到上次说的,柯里化的思路(具体原理参见

),大致形式是这样:

type Dependency =

    static member inline Inject f = 
        
        Instance.GetInstance() |> f  |> Dependency.Inject

这里使用一个类,而不是模块。原因我在后面说。当我调用Dependency.Inject时,函数接收到一个柯里化函数(这里是Foo),其后,框架会根据第一个参数的类型,把获取到的实例传给它,它就变成一个短一点的柯里化函数,继续传给 Dependency.Inject,最后直到 f 不再是个函数为止,思路是这样。但是现在这个函数只是接受函数的,所以我们再补一个守卫函数,提供递归终止的方式:

    static member inline Inject x: unit = x

我这里使用类的原因就是因为需要两个同名函数协同工作,而模块不支持重载。

但是现在Dependency.Inject可以工作了吗?并没有,提示需要更多批注来确定类型。但是需要填泛型参数就不叫自动了。这里为什么不能自动匹配呢,因为没有提前触发了泛型的静态解析。好,我们现在使用一个函数来延迟静态解析试试:

在文件头增加一个模块,我也取名为 Dependency,实在懒得再取名,这个不是必须的,它会引起困惑。

module Dependency =
    let inline inject< ^t, ^r, ^f when ( ^t or ^f): (static member Inject: ^f -> ^r)> fn =
        (( ^t or ^f): (static member Inject: ^f -> ^r) fn)

这个写起来有点麻烦,我稍微解释一下,F# 有两种泛型,动态解析的泛型(对应C#里使用的泛型)和静态解析的泛型:动态解析使用的泛型参数以'开始 ,而静态解析的泛型参数则以^开始,而现在我们使用静态的。 when后面就是泛型约束。

( ^t or ^f): (static member Inject: ^f -> ^r)

这里对 ^t^f 进行了约束,它们必须满足这样一个条件:^t或者^f具有一个名叫Inject的静态函数,其函数签名为:^f -> ^r (参数是^f类型,返回值是^r类型),这种约束,只要 ^t 或者 ^f 满足条件都可以执行。为什么出现这样设定的语法,我也不清楚,而且只有这种写法,才能触发编译器搜索。好,现在我们继续,我们把Dependency作为类型参数传进去,就能得到一个绑定了Dependency的函数:

let inline inject x = Dependency.inject<Dependency, _, _> x

Dependency类就变成这样子:

type Dependency =

    static member Inject x: unit = x

    static member inline Inject f = 

        let inline inject x = Dependency.inject<Dependency, _, _> x

        Instance.GetInstance() |> f  |> inject

现在,只要Instance.GetInstance() 能获取实例,就可以运作了。但是现在我们还没有实现Instance.GetInstance(),可以用Unchecked.defaultof<_>来测试一下,这个函数的功能等同于C#里的default,不过它在F#里是个动态解析的泛型属性。全部代码如下:

module Dependency =
    let inline inject< ^t, ^r, ^f when ( ^t or ^f): (static member Inject: ^f -> ^r)> fn =
        (( ^t or ^f): (static member Inject: ^f -> ^r) fn)

type Dependency =

    static member Inject x: unit = x

    static member inline Inject f = 

        let inline inject x = Dependency.inject<Dependency, _, _> x
        // 用Unchecked.defaultof代替测试
        Unchecked.defaultof<_>(*Instance.GetInstance()*) |> f  |> inject

let Foo(a: int) (b: int64) (c: DateTime) = 
    printf "%A:%d\n%A:%d\n%A: %A" (a.GetType()) a (b.GetType()) b (c.GetType()) c 

[<EntryPoint>]
let main _ =
    let _ = Dependency.Inject(Foo)
    let _ = Console.ReadLine() in 0

运行后打印如下:

System.Int32:0
System.Int64:0
System.DateTime: 0001/1/1 0:00:00

自动填仓成功了。下面我们继续把刚才没有完成Instance.GetInstance() 实现了,可以通过接口来做,但刚才测试用的是动态解析泛型,现在我们试试静态,静态解析独有的好处是,它可以限制得更精确,更随意:

首先,我们需要重载获取int int64DataTime的三个函数 ,重载需要用类:

type Instance =

    static member GetInstance() = 1

    static member GetInstance() = 2L

    static member GetInstance() = DateTime.Now

但是重载不能通过返回值辨认怎么办?我们添加一个不使用,但是跟返回值一样的参数,测试用可以随意一点,而且inline 函数最后会优化掉:

type Instance =

    static member GetInstance(_: int) = 1

    static member GetInstance(_: int64) = 2L

    static member GetInstance(_: DateTime) = DateTime.Now

然后我们故技重施,现在已经是轻车熟路了,在最上面Dependency 模块里添加静态泛型解析函数:

    let inline getInstance< ^t, ^a when ( ^t or ^a): (static member GetInstance: ^a -> ^a)> x =
        (( ^t or ^a): (static member GetInstance: ^a -> ^a) x )

然后在Instance 类里添加让外部调用的函数:

    static member inline GetInstance(): ^a =

        let inline getInstance x = Dependency.getInstance<Instance,_> x

        Unchecked.defaultof<_> |> getInstance 

好的,现在重新测试:

打印如下:

System.Int32:1
System.Int64:2
System.DateTime: 2020/4/30 15:52:37

总算全部完成了。

当然,这个依赖注入还不完美,譬如:

static member Inject x: unit = x

限制了Foo函数只能返回unit或者无返回值。

如果要支持其他返回值,则必须手动添加,假如要支持int返回值,就要添加:

    static member Inject x: int = x

有没有办法改成泛型呢,譬如:

type Dependency<'T> =

    static member Inject x: 'T = x

    static member inline Inject f = 

        let inline inject x = Dependency.inject<Dependency<_>, _, _> x

        Instance.GetInstance() |> f  |> inject

我们可以试试: Dependency<'T>内部是不会报错了,也就是说能够推断出一个确定类型,下面我们测试一下:

let Foo (a: int) (b: int64) (c: DateTime) = 
    printfn "a=%d\nb=%d\nc=%A" a b c

let x = Dependency<unit>.Inject(Foo)

语法上也不报错,我们再看看返回值 x的类型:

出大事了,x只推了一步就终止了,为什么呢?因为int64-> string->unit也可以作为函数类型传入。所以满足类型T它就不计算了。现在我们把x强制为unit,结果也于事无补,只是提示类型错误。

还有没有办法呢?有,需要一点技巧,就是加一个泛型约束,加什么约束呢,好像F#里没有什么约束是针对非函数的?

仔细想想还是有的,F# 比 C# 多了一个泛型约束,就是equality,.net 里所有对象和结构,逻辑上都继承于obj,所以都有.Equals 方法,F#加了equality好像是脱裤子放屁。但是F#在语法上规定了,函数不能是equality的,也就是两个函数不能用来比较是否相等。所以现在刚好派上用场了:

type Dependency<'T when 'T: equality> =

    static member Inject x: 'T = x

    static member inline Inject f = 

        let inline inject x = Dependency.inject<Dependency<_>, _, _> x

        Instance.GetInstance() |> f  |> inject

好,我们现在再看看x:,这次干脆不显式指定类型:

let x = Dependency<_>.Inject(Foo)

x 显示为:

v2-5e8cdf22e50857dc3771f96c5e009b63_720w.png

OK,F#做自动依赖注入的尝试就到此为止了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK