[关闭]
@atry 2017-11-16T10:08:03.000000Z 字数 8159 阅读 1432

神经网络与函数式编程(八)抽象工厂模式你懂吗?

神经网络与函数式编程

在本系列的神经网络与函数式编程(五)怎么让神经网络自动调参?中学习了插件的用法,能用插件添加类型和函数或者修改现有类型和函数的行为。在本篇文章中,我们将讨论DeepLearning.scala如何实现插件系统。


类型类Factory

在DeepLearning.scala中的插件都是普通trait,比如最简单的插件是空trait。比如:

  1. trait Plugin1
  2. trait Plugin2

Plugin1Plugin2都可以看成插件。我们先前已经知道,插件可以用Factory组装到一起:

  1. val plugin1WithPlugin2: Plugin1 with Plugin2 = Factory[Plugin1 with Plugin2].newInstance()

在这种简单情况下,上述代码创建了一个匿名对象,把Plugin1Plugin2两个插件混入到一起,等价于以下代码:

  1. val plugin1WithPlugin2: Plugin1 with Plugin2 = new Plugin1 with Plugin2 {}

Factory与Scala的new关键字都能创建匿名对象。不过,Factory是个类型类,可以配合类型参数使用,比如

  1. case class SeqFactory[A]() {
  2. def createTenObjects[ConstructorForA](
  3. implicit factory: Factory.Aux[A, ConstructorForA],
  4. constructorConstraint: (ConstructorForA <:< (() => A))
  5. ): Seq[A] = {
  6. Seq.fill(10)(constructorConstraint(factory.newInstance)())
  7. }
  8. }
  9. val fooWithBarSeq: Seq[Plugin1 with Plugin2] = SeqFactory[Plugin1 with Plugin2]().createTenObjects

以上代码创建了case class SeqFactory[A],当用户调用SeqFactory[A]中的createTenObjects方法时,会利用根据隐式提供的Factory创建出包含10个ASeq

其中Factory是个依赖类型的类型类,能通过隐式宏自动实体化。

  1. trait Factory[Output] {
  2. type Constructor
  3. val newInstance: Constructor
  4. }
  5. object Factory {
  6. type Aux[A, Constructor0] = Factory[A] {
  7. type Constructor = Constructor0
  8. }
  9. implicit def apply[Output]: Factory[Output] = macro ...
  10. }

由于Factory.apply是个宏,所以Factory[Plugin1 with Plugin2].newInstance()会在编译时被替换成类似这样的代码:

  1. (new Factory[Plugin1 with Plugin2] {
  2. type Constructor = () => Plugin1 with Plugin2
  3. val newInstance = { () =>
  4. new Plugin1 with Plugin2 {}
  5. }
  6. }).newInstance()

而且,Factory.apply是个隐式宏,能在搜索隐式参数implicit factory: Factory.Aux[A, ConstructorForA]时也触发宏。这是因为Factory.Aux[A, ConstructorForA]等价于Factory[A] { type Constructor = ConstructorForA },其中ConstructorForA是个有待确定的未知类型参数。

比如,在代码SeqFactory[Plugin1 with Plugin2]().createTenObjects中,已经确定APlugin1 with Plugin2,但ConstructorForA尚未确定。当其中第一个隐式参数implicit factory: Factory.Aux[A, ConstructorForA]被宏替换为后,等价于以下代码:

  1. SeqFactory[Plugin1 with Plugin2]().createTenObjects(
  2. // 第一个隐式参数
  3. new Factory[Plugin1 with Plugin2] {
  4. type Constructor = () => Plugin1 with Plugin2
  5. val newInstance = { () =>
  6. new Plugin1 with Plugin2 {}
  7. }
  8. },
  9. // 第二个隐式参数,尚未确定
  10. ???
  11. )

其中的第一个隐式参数会与Factory.Aux[A, ConstructorForA]进行type unification,从而帮助Scala编译器确定ConstructorForA的类型为() => Plugin1 with Plugin2,因此第二个隐式参数的类型是(() => Plugin1 with Plugin2) <:< (() => Plugin1 with Plugin2),可以通过Predef.$conforms[() => Plugin1 with Plugin2]召唤。

那么,最终编译器推断的类型参数和隐式值如下:

  1. SeqFactory[Plugin1 with Plugin2]().createTenObjects[
  2. () => Plugin1 with Plugin2 // 推断出的类型参数
  3. ](
  4. // 第一个隐式参数
  5. new Factory[Plugin1 with Plugin2] {
  6. type Constructor = () => Plugin1 with Plugin2
  7. val newInstance = { () =>
  8. new Plugin1 with Plugin2 {}
  9. }
  10. },
  11. // 第二个隐式参数
  12. Predef.$conforms[() => Plugin1 with Plugin2]
  13. )

当第二个隐式参数召唤出来时,就能证明第一个隐式参数factory内部的Constructor类型是() => Plugin1 with Plugin2的子类型。换句话说,在createTenObjects中只要调用constructorConstraint(factory.newInstance),就能获得() => Plugin1 with Plugin2类型的Constructor,用来创建Plugin1 with Plugin2了。

抽象方法

先前在神经网络与函数式编程(五)怎么让神经网络自动调参?中,我们已经知道,插件可以包含抽象方法。这些方法会自动变成Factory.Constructor的参数,可以在Factory.newInstance调用时传入,比如:

  1. trait MyAbstractTrait {
  2. val myAbstractField: Int
  3. def myAbstractGetter: Double
  4. def myAbstractMethod0(): Float
  5. def myAbstractMethod1(parameter: String): Unit
  6. }
  7. val myAbstractTrait: MyAbstractTrait = Factory[MyAbstractTrait].newInstance(
  8. myAbstractField = 42,
  9. myAbstractGetter = 1.0,
  10. myAbstractMethod0 = { () => 2.0f },
  11. myAbstractMethod1 = { parameter: String => }
  12. )

其中,Factory[myAbstractTrait]调用了Factory.apply宏,会被Scala编译器替换为:

  1. new Factory[myAbstractTrait] {
  2. type Constructor = ((Int, => Double, () => Float, String => Unit) => MyAbstractTrait) {
  3. def apply(
  4. myAbstractField: Int,
  5. myAbstractGetter: => Double,
  6. myAbstractMethod0: () => Float,
  7. myAbstractMethod1: String => Unit
  8. ): MyAbstractTrait
  9. }
  10. val newInstance: Constructor = { (_myAbstractField: Int,
  11. _myAbstractGetter: => Double,
  12. _myAbstractMethod0: () => Float,
  13. _myAbstractMethod1: String => Unit) =>
  14. new MyAbstractTrait {
  15. val myAbstractField: Int = _myAbstractField
  16. def myAbstractGetter: Double = _myAbstractGetter
  17. def myAbstractMethod0(): Float = _myAbstractMethod0()
  18. def myAbstractMethod1(parameter: String): Unit = _myAbstractMethod1(parameter)
  19. }
  20. }
  21. }

这样一来,Constructor是一个函数,接受的参数与MyAbstractTrait中定义的myAbstractFieldmyAbstractGettermyAbstractMethod0myAbstractMethod1一一对应,newInstance的类型是Constructor,那么就可以向newInstance传入参数,创建MyAbstractTrait了。其中MyAbstractTrait中的字段myAbstractField对应了一个call-by-value参数,无参数的方法myAbstractGetter则对应了call-by-name参数,而有参数列表的方法myAbstractMethod0myAbstractMethod0则对应了回调函数参数。

此外,Constructor还是个细化类型(refinement type),重新定义了apply方法中的参数名称。所以,当调用newInstance时,可以使用命名参数。

注入抽象方法

Factory还支持注解@inject,可以自动用隐式值实现插件中抽象方法,比如:

  1. trait InjectedTrait {
  2. def abstractMethod: Int
  3. @inject def injectedAbstractMethod: Ordering[Int]
  4. }
  5. Factory[InjectedTrait].newInstance(abstractMethod = 42)

在这段代码中,abstractMethodinjectedAbstractMethod都是抽象方法,但实现方式不同。abstractMethod需要用户手动实现,而injectedAbstractMethod则被自动实现为一个隐式值。

Factory[InjectedTrait]在宏展开后会被替换为:

  1. new Factory[InjectedTrait] {
  2. type Constructor = ((=> Int) => InjectedTrait) {
  3. def apply(injectedAbstractMethod: => Ordering[Int]): InjectedTrait
  4. }
  5. val newInstance: Constructor = { (_injectedAbstractMethod: => Ordering[Int]) =>
  6. new InjectedTrait {
  7. def abstractMethod: Int = _injectedAbstractMethod
  8. def injectedAbstractMethod = The[Ordering[Int]].value
  9. }
  10. }
  11. }

其中The[Ordering[Int]].value类似Predef.implicitly,可以用来召唤Ordering[Int]的隐式值。

利用@inject可以向一个插件注入另一个插件所需的类型,甚至可以递归注入另一个Factory,比如:

  1. trait FieldInitializer {
  2. type Field
  3. @inject val fieldFactory: Factory[Field]
  4. @inject def constructorAsFunction: fieldFactory.Constructor <:< (() => Field)
  5. val field: Field = {
  6. constructorAsFunction(fieldFactory.newInstance)()
  7. }
  8. }
  9. trait FieldApiProvider1 {
  10. trait FieldApi1 {
  11. def method1() = println("method1 in in FieldApi1")
  12. }
  13. type Field = FieldApi1
  14. }
  15. val mixed: FieldInitializer with FieldApiProvider1 = Factory[FieldInitializer with FieldApiProvider1].newInstance()
  16. mixed.field.method1() // 输出 "method1 in in FieldApi1"

Factory[FieldInitializer with FieldApiProvider1]宏展开后,Field的类型被设为FieldApi1,那么标有@inject的抽象字段fieldFactory会被实现为Factory[FieldApi1]类型的隐式值。然后,当fieldFactory被成功召唤以后,另一处标有@inject的抽象方法constructorAsFunction可以证明fieldFactory.Constructor是个能创建FieldApi1的零参数函数。最后FieldInitializer再利用constructorAsFunctionfieldFactory就能把FieldApi1类型的field创建出来。

抽象类型

Factory除了能自动实现抽象方法和字段以外,还能自动把抽象类型实现为该抽象类型的上界类型。比如:

  1. trait HasAbstractType {
  2. type MyAbstractType >: Null <: String
  3. }
  4. val hasAbstractType = Factory[HasAbstractType].newInstance()

hasAbstractType.MyAbstractType的类型是抽象类型MyAbstractType的上界String而不是下界Null,而hasAbstractType的类型是HasAbstractType { type MyAbstractType = String }

Factory[HasAbstractType]宏展开后是这样的:

  1. new Factory[HasAbstractType] {
  2. type Constructor = () => HasAbstractType { type MyAbstractType = String }
  3. val newInstance: Constructor = { () =>
  4. new HasAbstractType { type MyAbstractType = String }
  5. }
  6. }

如果Factory要创建的类型混入了多个插件,而且这些插件包含同名的抽象类型,那么Factory创建的匿名对象会把该抽象类型实现为所有插件中的该抽象类型的上界的共同子类型。DeepLearning.scala利用了这一行为,使得一个插件可以向抽象类型中添加功能,或者修改其他插件提供的函数的行为。比如:

  1. trait FieldInitializer {
  2. type Field
  3. @inject val fieldFactory: Factory[Field]
  4. @inject def constructorAsFunction: fieldFactory.Constructor <:< (() => Field)
  5. val field: Field = {
  6. constructorAsFunction(fieldFactory.newInstance)()
  7. }
  8. }
  9. trait HasMethod1 {
  10. def method1(): Unit
  11. }
  12. trait FieldApiProvider1 {
  13. trait FieldApi1 extends HasMethod1 {
  14. def method1() = println("method1 in in FieldApi1")
  15. }
  16. type Field <: FieldApi1
  17. }
  18. trait FieldApiProvider2 {
  19. trait FieldApi2 extends HasMethod1 {
  20. def method2() = println("method1 in FieldApi2")
  21. abstract override def method1() = {
  22. super.method1()
  23. println("method1 in FieldApi2")
  24. }
  25. }
  26. type Field <: FieldApi2
  27. }
  28. val mixedWithProvider1 = Factory[FieldInitializer with FieldApiProvider1].newInstance()
  29. mixedWithProvider1.field.method1() // 输出 "method1 in in FieldApi1"
  30. val mixedWithProvider1AndProvider2 = Factory[FieldInitializer with FieldApiProvider1 with FieldApiProvider2].newInstance()
  31. mixedWithProvider1AndProvider2.field.method1() // 输出 "method1 in in FieldApi1" 和 "method1 in FieldApi2"
  32. mixedWithProvider1AndProvider2.field.method2() // 输出 "method2 in FieldApi2"

mixedWithProvider1启用了插件FieldInitializerFieldApiProvider1。其中FieldInitializer负责创建fieldFieldApiProvider1负责为field提供method1功能。

mixedWithProvider1除了启用插件FieldInitializerFieldApiProvider1以外,还额外启用了FieldApiProvider2FieldApiProvider2除了为field新增了method2以外,还会修改原本FieldApiProvider1中的method1的功能。FieldApiProvider2中的FieldApi2method1的修饰符是abstract override,这意味着,FieldApi2的实现者不必知道用户会把FieldApi2与哪些其他trait混入到一起,就可以调用其他trait中的method1实现。

与以上FieldApiProvider2的机制类似,在DeepLearning.scala中,DoubleLayerDoubleWeight等抽象类型的行为,也是由启用的插件共同决定的。

结论

DeepLearning.scala使用Factory组装插件,能够自动实现插件中的抽象成员,具体方式如下:

插件中如何定义抽象成员 Factory如何实现抽象成员
抽象字段call-by-value参数
无参数列表的抽象方法call-by-name参数
有参数列表的抽象方法函数类型的参数
@inject注解的抽象方法隐式值
抽象类型该抽象类型的上界类型

利用Factory实现抽象成员的能力,DeepLearning.scala的插件系统能向系统中注入一切功能,比如修改神经网络的优化方式、添加新的原子操作、网络层或者子网络、记录日志、甚至替换掉整个后端。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注